Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ tsconfig.tsbuildinfo
# Playwright
playwright-report
test-results
/playwright/.cache
/playwright/.cache

# Ignore temp folder for streaming demo
**/temp/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ The modernization steps:
- bump version to `v3.0.0` as a `major` release (_the original project version was in the `2.x` range._)
- note that the changelog did not exists prior to `v3.0.0`
- v4.x is now ESM-Only
- new Streaming API for large datasets

The project now requires only 1 small dependency which is [fflate](https://github.com/101arrowz/fflate).

Expand Down
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"recommended": true,
"complexity": {
"noForEach": "off",
"noStaticOnlyClass": "off"
"noStaticOnlyClass": "off",
"noUselessSwitchCase": "off"
},
"performance": {
"noBarrelFile": "off",
Expand Down
3 changes: 2 additions & 1 deletion docs/TOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@
- [Theming Tables](theming-tables.md)
- [Tables Summaries](tables-summaries.md)
- [Adding Headers and Footers to a Worksheet](worksheet-headers-footers.md)
- [Inserting images into spreadsheets](inserting-pictures.md)
- [Inserting images into spreadsheets](inserting-pictures.md)
- [Streaming Excel Export](streaming.md)
66 changes: 66 additions & 0 deletions docs/streaming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Streaming Excel Export

Streaming export is designed for large datasets, providing better performance and memory efficiency in both browser and NodeJS environments. The API and features are the same as the regular export and the features like formulas, alignment, borders, and more are all supported.

## Why Streaming?

Traditional export methods generate the entire Excel file in memory, which can hang the browser or consume excessive resources for large datasets. Streaming solves this by generating and delivering the file in chunks.

## Usage in the Browser

Use `createExcelFileStream` to export data as a stream. You can process chunks and update progress as needed.

```ts
import { createWorkbook, createExcelFileStream } from 'excel-builder-vanilla';

const workbook = createWorkbook();
const worksheet = workbook.createWorksheet({ name: 'Demo' });
worksheet.setData([
['Artist', 'Album', 'Price'],
['Buckethead', 'Albino Slug', 8.99],
// ... more rows
]);
workbook.addWorksheet(worksheet);

const stream = createExcelFileStream(workbook, { chunkSize: 1000 });
const chunks: Uint8Array[] = [];
for await (const chunk of stream as AsyncIterable<Uint8Array>) {
chunks.push(chunk);
// Optionally update progress bar here
}
const blob = new Blob(chunks, { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = URL.createObjectURL(blob);
// Download with anchor tag
```

## Usage in NodeJS

Streaming in NodeJS works similarly, but you can pipe the output directly to a file stream.

```js
import fs from 'node:fs';
import { createWorkbook, createExcelFileStream } from 'excel-builder-vanilla';

const workbook = createWorkbook();
// ... add data and worksheets

const output = fs.createWriteStream('output.xlsx');
for await (const chunk of createExcelFileStream(workbook, { chunkSize: 1000 })) {
output.write(chunk);
}
output.end();
```

> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder.

## Supported Features

All features such as formulas, alignment, borders, styles, and images work with streaming export. The only difference is how the file is delivered.

## See Also

- [Formulas](formulas.md)
- [Alignment](alignment.md)
- [Borders](fonts-and-colors.md)
- [Tables](tables.md)
- [Headers/Footers](worksheet-headers-footers.md)
19 changes: 19 additions & 0 deletions docs/workbook-create.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,22 @@ const workbook = new Workbook();
This will eventually require you to include the 'excel-builder' module so you can export the workbook, so it's more verbose. However, this is also the best option for creating templates and the like.

Workbooks with no worksheet (i.e. data) will build, but Excel will throw an error while attempting to open it.

---

## NodeJS Usage Example

You can use excel-builder-vanilla in NodeJS to generate and save Excel files directly to disk:

```js
import fs from 'node:fs';
import { createWorkbook, createExcelFile } from 'excel-builder-vanilla';

const workbook = createWorkbook();
// ... add worksheets and data

const buffer = createExcelFile(workbook);
fs.writeFileSync('output.xlsx', buffer);
```

> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder.
24 changes: 24 additions & 0 deletions docs/worksheet-add-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,27 @@ artistWorkbook.addWorksheet(albumList);
const data = createExcelFile(artistWorkbook);
downloader('Artist WB.xlsx', data);
```

---

## NodeJS Usage Example

You can add data to a worksheet and export in NodeJS:

```js
import fs from 'node:fs';
import { createWorkbook, createExcelFile } from 'excel-builder-vanilla';

const workbook = createWorkbook();
const sheet = workbook.createWorksheet({ name: 'Demo' });
sheet.setData([
['Artist', 'Album', 'Price'],
['Buckethead', 'Albino Slug', 8.99],
]);
workbook.addWorksheet(sheet);

const buffer = createExcelFile(workbook);
fs.writeFileSync('output.xlsx', buffer);
```

> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder.
24 changes: 24 additions & 0 deletions docs/worksheet-create.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,27 @@ This will set the 'name' of the worksheet to 'Account Summary' so it doesn't sho
```ts
const accountSummarySheet = workbook.createWorksheet({ name: 'Account Summary' });
```

---

## NodeJS Usage Example

Worksheets can be created and exported in NodeJS just like in the browser:

```js
import fs from 'node:fs';
import { createWorkbook, createExcelFile } from 'excel-builder-vanilla';

const workbook = createWorkbook();
const sheet = workbook.createWorksheet({ name: 'Demo' });
sheet.setData([
['Artist', 'Album', 'Price'],
['Buckethead', 'Albino Slug', 8.99],
]);
workbook.addWorksheet(sheet);

const buffer = createExcelFile(workbook);
fs.writeFileSync('output.xlsx', buffer);
```

> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder.
26 changes: 26 additions & 0 deletions docs/worksheet-headers-footers.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,29 @@ artistWorkbook.addWorksheet(albumList);
const data = createExcelFile(artistWorkbook);
downloader('Artist WB.xlsx', data);
```

---

## NodeJS Usage Example

Headers and footers work in NodeJS as well:

```js
import fs from 'node:fs';
import { createWorkbook, createExcelFile } from 'excel-builder-vanilla';

const workbook = createWorkbook();
const sheet = workbook.createWorksheet({ name: 'Demo' });
sheet.setData([
['Artist', 'Album', 'Price'],
['Buckethead', 'Albino Slug', 8.99],
]);
sheet.setHeader(['Left', 'Center', 'Right']);
sheet.setFooter(['Date: &D', '&A', 'Page &P of &N']);
workbook.addWorksheet(sheet);

const buffer = createExcelFile(workbook);
fs.writeFileSync('output.xlsx', buffer);
```

> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"url": "https://ko-fi.com/ghiscoding"
},
"scripts": {
"clean": "remove --glob **/dist **/tsconfig.tsbuildinfo",
"clean": "remove --glob \"**/dist **/tsconfig.tsbuildinfo\"",
"prebuild": "pnpm run clean && pnpm run biome:lint:write && pnpm run biome:format:write",
"build": "pnpm -r --stream build",
"build:demo": "pnpm -r --stream --filter \"./packages/demo/**\" build",
Expand All @@ -45,7 +45,9 @@
"roll-new-release": "pnpm build && pnpm new-version && pnpm new-publish",
"serve:demo": "pnpm -r --stream --filter \"./packages/demo/**\" dev",
"test": "vitest --watch --config ./vitest/vitest.config.mts",
"test:coverage": "vitest --coverage --config ./vitest/vitest.config.mts"
"test:coverage": "vitest --coverage --config ./vitest/vitest.config.mts",
"demo:node:streaming": "node ./packages/demo/node-examples/node-streaming-demo.mjs",
"demo:node:non-streaming": "node ./packages/demo/node-examples/node-non-streaming-demo.mjs"
},
"engines": {
"node": "^20.17.0 || >=22.9.0",
Expand Down
52 changes: 52 additions & 0 deletions packages/demo/node-examples/node-non-streaming-demo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import fs from 'node:fs';
import path from 'node:path';
import { createExcelFile, createWorkbook } from 'excel-builder-vanilla';

// Build data array (same as streaming example)
const ROWS = 1000;
const dataArray = [];

// Add header row at row 0, merged and styled
const workbook = createWorkbook();
const worksheet = workbook.createWorksheet({ name: 'Demo Non-Streaming' });

// Create a format for the header row
const stylesheet = workbook.getStyleSheet();
const headerFormat = stylesheet.createFormat({
alignment: { horizontal: 'center' },
font: { bold: true, color: 'FF2b995d', size: 13 },
});

dataArray.push([{ value: 'NodeJS Non-Streaming Output', metadata: { style: headerFormat.id } }]);
dataArray.push(['ID', 'Name', 'Score']);
for (let i = 1; i <= ROWS; i++) {
dataArray.push([i, `User ${i}`, Math.floor(Math.random() * 100)]);
}
// Add a formula cell for the total score
dataArray.push(['', 'Total', { value: `SUM(C2:C${ROWS + 2})`, metadata: { type: 'formula' } }]);

worksheet.setData(dataArray);
worksheet.mergeCells('A1', 'C1');
workbook.addWorksheet(worksheet);

// Ensure temp folder exists
const tempDir = path.resolve(process.cwd(), 'temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir);
}
const outputPath = path.join(tempDir, 'node-non-streaming-example15.xlsx');

(async () => {
let buffer = await createExcelFile(workbook, {
outputType: 'Uint8Array',
fileFormat: 'xlsx',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
downloadType: 'node',
});
// If buffer is a Blob (browser fallback), convert to ArrayBuffer
if (buffer instanceof Blob) {
buffer = new Uint8Array(await buffer.arrayBuffer());
}
fs.writeFileSync(outputPath, buffer);
console.log(`Excel file written to ${outputPath}`);
})();
51 changes: 51 additions & 0 deletions packages/demo/node-examples/node-streaming-demo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fs from 'node:fs';
import path from 'node:path';
import { createExcelFileStream, createWorkbook } from 'excel-builder-vanilla';

// Build data array (same as browser example)
const ROWS = 1000;
const dataArray = [];

// Add header row at row 0, merged and styled
const workbook = createWorkbook();
const worksheet = workbook.createWorksheet({ name: 'Demo Streaming' });

// Create a format for the header row
const stylesheet = workbook.getStyleSheet();
const headerFormat = stylesheet.createFormat({
alignment: { horizontal: 'center' },
font: { bold: true, color: 'FF2b995d', size: 13 },
});

dataArray.push([{ value: 'NodeJS Streaming Output', metadata: { style: headerFormat.id } }]);
dataArray.push(['ID', 'Name', 'Score']);
for (let i = 1; i <= ROWS; i++) {
dataArray.push([i, `User ${i}`, Math.floor(Math.random() * 100)]);
}
dataArray.push(['', 'Total', { value: `SUM(C2:C${ROWS + 2})`, metadata: { type: 'formula' } }]);

worksheet.setData(dataArray);
worksheet.mergeCells('A1', 'C1');
workbook.addWorksheet(worksheet);

// Ensure temp folder exists
const tempDir = path.resolve(process.cwd(), 'temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir);
}
const outputPath = path.join(tempDir, 'node-streaming-example15.xlsx');
const output = fs.createWriteStream(outputPath);

(async () => {
for await (const chunk of createExcelFileStream(workbook, {
zipOptions: {},
outputType: 'Uint8Array',
fileFormat: 'xlsx',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
downloadType: 'node',
})) {
output.write(chunk);
}
output.end();
console.log(`Excel file written to ${outputPath}`);
})();
4 changes: 3 additions & 1 deletion packages/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
"preview": "vite preview",
"stream:excel": "node ./node-examples/node-streaming-demo.mjs",
"nonstream:excel": "node ./node-examples/node-non-streaming-demo.mjs"
},
"dependencies": {
"@excel-builder-vanilla/types": "workspace:*",
Expand Down
6 changes: 6 additions & 0 deletions packages/demo/src/app-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import Example11 from './examples/example11.js';
import Example12 from './examples/example12.js';
import Example13 from './examples/example13.js';
import Example14 from './examples/example14.js';
import Example15 from './examples/example15.js';
import Example16 from './examples/example16.js';
import Example17 from './examples/example17.js';
import GettingStarted from './getting-started.js';

export const navbarRouting = [
Expand Down Expand Up @@ -42,6 +45,9 @@ export const exampleRouting = [
{ name: 'example12', view: '/src/examples/example12.html', viewModel: Example12, title: '12- Worksheet Headers/Footers' },
{ name: 'example13', view: '/src/examples/example13.html', viewModel: Example13, title: '13- Pictures with 2 anchors' },
{ name: 'example14', view: '/src/examples/example14.html', viewModel: Example14, title: '14- Pictures with different anchors' },
{ name: 'example15', view: '/src/examples/example15.html', viewModel: Example15, title: '15- Streaming Excel Export' },
{ name: 'example16', view: '/src/examples/example16.html', viewModel: Example16, title: '16- Streaming Features Demo' },
{ name: 'example17', view: '/src/examples/example17.html', viewModel: Example17, title: '17- Streaming Export with Images' },
],
},
];
1 change: 0 additions & 1 deletion packages/demo/src/examples/example01.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { downloadExcelFile, Workbook } from 'excel-builder-vanilla';
// import type { ExcelStyleInstruction } from '@excel-builder-vanilla/types';

import './example01.scss';

Expand Down
6 changes: 4 additions & 2 deletions packages/demo/src/examples/example02.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ <h2 class="bd-title">
</span>
</h2>
<div class="demo-subtitle">
The column <code>width</code>attribute will set a width. The <code>hidden</code>attribute will hide the column in Excel. The example
below has the "Artist" column wider and the next column "Album" to be hidden in the exported Excel file.
The column <code>width</code>
attribute will set a width. The <code>hidden</code>
attribute will hide the column in Excel. The example below has the "Artist" column wider and the next column "Album" to be hidden in
the exported Excel file.
</div>
</div>
</div>
Expand Down
Loading