@farhantallei/excel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -0
- package/dist/index.cjs +116 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/types.cjs +4 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +28 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# @farhantallei/excel
|
|
2
|
+
|
|
3
|
+
> Type-safe, declarative Excel (XLSX) export for Node.js server environments.
|
|
4
|
+
|
|
5
|
+
Build Excel files using a clean configuration object — with typed columns, formatting, and row mapping. Designed for server-side execution only.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- ✅ Declarative worksheet configuration
|
|
12
|
+
- ✅ Fully type-safe (TypeScript-first)
|
|
13
|
+
- ✅ Column metadata (width, type, format)
|
|
14
|
+
- ✅ Generic row mapping
|
|
15
|
+
- ✅ Server-only safe (no browser XLSX usage)
|
|
16
|
+
- ✅ Zero global state
|
|
17
|
+
- ✅ Clean separation of concerns
|
|
18
|
+
- ✅ JSON import from Excel
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @farhantallei/excel
|
|
26
|
+
# or
|
|
27
|
+
yarn add @farhantallei/excel
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Quick Start (Export)
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { exportToExcel } from "@farhantallei/excel"
|
|
38
|
+
|
|
39
|
+
const blob = await exportToExcel({
|
|
40
|
+
sheetName: "Users",
|
|
41
|
+
header: ["No", "Name", "Active"],
|
|
42
|
+
columns: [
|
|
43
|
+
{ width: 50, type: "number" },
|
|
44
|
+
{ width: 200, type: "string" },
|
|
45
|
+
{ width: 100, type: "string" },
|
|
46
|
+
],
|
|
47
|
+
data: users,
|
|
48
|
+
mapRow: (user, index) => [
|
|
49
|
+
index + 1,
|
|
50
|
+
user.name,
|
|
51
|
+
user.active ? "Yes" : "No",
|
|
52
|
+
],
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### Send to Client (Next.js Example)
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// Server Action / Route Handler
|
|
60
|
+
return new Response(blob, {
|
|
61
|
+
headers: {
|
|
62
|
+
"Content-Type":
|
|
63
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
64
|
+
"Content-Disposition": 'attachment; filename="users.xlsx"',
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Import Excel → JSON
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { readExcelToJson } from "@farhantallei/excel"
|
|
73
|
+
|
|
74
|
+
const data = await readExcelToJson<User>(file)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Optional sheet selection:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
await readExcelToJson<User>(file, {
|
|
81
|
+
sheetName: "Users",
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## API
|
|
88
|
+
|
|
89
|
+
### `exportToExcel<T>(config)`
|
|
90
|
+
|
|
91
|
+
Generate an Excel `Blob` from a declarative configuration.
|
|
92
|
+
|
|
93
|
+
#### Config
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
{
|
|
97
|
+
sheetName: string
|
|
98
|
+
header: string[]
|
|
99
|
+
columns: {
|
|
100
|
+
width?: number
|
|
101
|
+
type?: "string" | "number" | "date"
|
|
102
|
+
format?: string
|
|
103
|
+
}[]
|
|
104
|
+
data: T[]
|
|
105
|
+
mapRow: (item: T, index: number) => unknown[]
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Returns
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
Promise<Blob>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `readExcelToJson<T>(file, options?)`
|
|
116
|
+
|
|
117
|
+
Read an Excel file and convert the first (or specified) worksheet into JSON.
|
|
118
|
+
|
|
119
|
+
#### Options
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
{
|
|
123
|
+
sheetName?: string
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Returns
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
Promise<T[]>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Error Handling
|
|
134
|
+
|
|
135
|
+
* `APIError` – Standardized error class for HTTP requests
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
try {
|
|
139
|
+
await fetcher("/users")({ method: "GET" })
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err instanceof APIError) {
|
|
142
|
+
console.error(err.status, err.message)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## ⚠️ Environment Constraint
|
|
150
|
+
|
|
151
|
+
This library is server-side only.
|
|
152
|
+
|
|
153
|
+
It must be executed in:
|
|
154
|
+
|
|
155
|
+
- Node.js
|
|
156
|
+
- Next.js Server Actions
|
|
157
|
+
- Next.js Route Handlers
|
|
158
|
+
- Any server runtime
|
|
159
|
+
|
|
160
|
+
It **must not** be used in the browser, as `xlsx` is not browser-compatible in this setup.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 🏗 Design Principles
|
|
165
|
+
|
|
166
|
+
- No global state mutation
|
|
167
|
+
- No external I/O
|
|
168
|
+
- XLSX logic fully encapsulated
|
|
169
|
+
- Caller provides only configuration
|
|
170
|
+
- Strict TypeScript compatibility
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Contributing
|
|
175
|
+
|
|
176
|
+
PRs and issues are welcome. please fork the repository, create a feature branch, ensure all tests and type checks pass, and submit a pull request.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var XLSX3 = require('xlsx');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var XLSX3__default = /*#__PURE__*/_interopDefault(XLSX3);
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
function buildSheet(config) {
|
|
11
|
+
const { header, headerRows, data, mapRow, footerRows } = config;
|
|
12
|
+
const rows = [];
|
|
13
|
+
if (headerRows && headerRows.length > 0) {
|
|
14
|
+
for (let i = 0; i < headerRows.length; i++) {
|
|
15
|
+
const row = headerRows[i];
|
|
16
|
+
if (row) rows.push(row);
|
|
17
|
+
}
|
|
18
|
+
} else if (header && header.length > 0) {
|
|
19
|
+
rows.push(header);
|
|
20
|
+
}
|
|
21
|
+
for (let i = 0; i < data.length; i++) {
|
|
22
|
+
const item = data[i];
|
|
23
|
+
if (item) rows.push(mapRow(item, i));
|
|
24
|
+
}
|
|
25
|
+
if (footerRows && footerRows.length > 0) {
|
|
26
|
+
for (let i = 0; i < footerRows.length; i++) {
|
|
27
|
+
const footerRow = footerRows[i];
|
|
28
|
+
if (footerRow) rows.push(footerRow);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const ws = XLSX3__default.default.utils.aoa_to_sheet(rows);
|
|
32
|
+
if (config.merges?.length) {
|
|
33
|
+
ws["!merges"] = config.merges;
|
|
34
|
+
}
|
|
35
|
+
const totalRows = rows.length;
|
|
36
|
+
const headerRowCount = config.headerRowCount ?? (config.headerRows ? config.headerRows.length : 1);
|
|
37
|
+
return { ws, totalRows, headerRowCount };
|
|
38
|
+
}
|
|
39
|
+
function applyColumnConfig({
|
|
40
|
+
ws,
|
|
41
|
+
columns,
|
|
42
|
+
totalRows,
|
|
43
|
+
headerRowCount
|
|
44
|
+
}) {
|
|
45
|
+
ws["!cols"] = columns.map((col) => ({ wpx: col.width ?? 100 }));
|
|
46
|
+
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
|
|
47
|
+
const col = columns[colIndex];
|
|
48
|
+
if (!col) continue;
|
|
49
|
+
if (!col.type && !col.format) continue;
|
|
50
|
+
const colLetter = XLSX3__default.default.utils.encode_col(colIndex);
|
|
51
|
+
for (let row = headerRowCount; row < totalRows; row++) {
|
|
52
|
+
const cellAddress = `${colLetter}${row + 1}`;
|
|
53
|
+
const cell = ws[cellAddress];
|
|
54
|
+
if (!cell) continue;
|
|
55
|
+
switch (col.type) {
|
|
56
|
+
case "number":
|
|
57
|
+
if (typeof cell.v === "number") {
|
|
58
|
+
cell.t = "n";
|
|
59
|
+
cell.z = col.format || "#,##0";
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case "date": {
|
|
63
|
+
const dt = new Date(cell.v);
|
|
64
|
+
if (!Number.isNaN(dt.getTime())) {
|
|
65
|
+
cell.t = "d";
|
|
66
|
+
cell.v = dt;
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
cell.t = "s";
|
|
72
|
+
}
|
|
73
|
+
if (col.format) {
|
|
74
|
+
cell.z = col.format;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function writeToBlob(ws, sheetName) {
|
|
80
|
+
const wb = XLSX3__default.default.utils.book_new();
|
|
81
|
+
XLSX3__default.default.utils.book_append_sheet(wb, ws, sheetName);
|
|
82
|
+
const wbout = XLSX3__default.default.write(wb, { bookType: "xlsx", type: "array" });
|
|
83
|
+
return new Blob([wbout], {
|
|
84
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/index.ts
|
|
89
|
+
function exportToExcel(config) {
|
|
90
|
+
const { ws, totalRows, headerRowCount } = buildSheet(config);
|
|
91
|
+
applyColumnConfig({
|
|
92
|
+
ws,
|
|
93
|
+
columns: config.columns,
|
|
94
|
+
totalRows,
|
|
95
|
+
headerRowCount
|
|
96
|
+
});
|
|
97
|
+
return writeToBlob(ws, config.sheetName);
|
|
98
|
+
}
|
|
99
|
+
async function readExcelToJson(file) {
|
|
100
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
101
|
+
const workbook = XLSX3__default.default.read(arrayBuffer, { type: "array" });
|
|
102
|
+
const sheetName = workbook.SheetNames[0];
|
|
103
|
+
if (!sheetName) {
|
|
104
|
+
throw new Error("The Excel file does not contain any worksheets.");
|
|
105
|
+
}
|
|
106
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
107
|
+
if (!worksheet) {
|
|
108
|
+
throw new Error(`Worksheet "${sheetName}" could not be found.`);
|
|
109
|
+
}
|
|
110
|
+
return XLSX3__default.default.utils.sheet_to_json(worksheet, { defval: "" });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
exports.exportToExcel = exportToExcel;
|
|
114
|
+
exports.readExcelToJson = readExcelToJson;
|
|
115
|
+
//# sourceMappingURL=index.cjs.map
|
|
116
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/builder.ts","../src/formatter.ts","../src/writer.ts","../src/index.ts"],"names":["XLSX"],"mappings":"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","file":"index.cjs","sourcesContent":["import XLSX from \"xlsx\"\n\nimport type { ExcelExportConfig } from \"./types\"\n\nexport function buildSheet<T>(config: ExcelExportConfig<T>) {\n\tconst { header, headerRows, data, mapRow, footerRows } = config\n\n\tconst rows: (string | number | Date | null)[][] = []\n\n\tif (headerRows && headerRows.length > 0) {\n\t\tfor (let i = 0; i < headerRows.length; i++) {\n\t\t\tconst row = headerRows[i]\n\t\t\tif (row) rows.push(row)\n\t\t}\n\t} else if (header && header.length > 0) {\n\t\trows.push(header)\n\t}\n\n\tfor (let i = 0; i < data.length; i++) {\n\t\tconst item = data[i]\n\t\tif (item) rows.push(mapRow(item, i))\n\t}\n\n\tif (footerRows && footerRows.length > 0) {\n\t\tfor (let i = 0; i < footerRows.length; i++) {\n\t\t\tconst footerRow = footerRows[i]\n\t\t\tif (footerRow) rows.push(footerRow)\n\t\t}\n\t}\n\n\tconst ws = XLSX.utils.aoa_to_sheet(rows)\n\n\tif (config.merges?.length) {\n\t\tws[\"!merges\"] = config.merges\n\t}\n\n\tconst totalRows = rows.length\n\n\tconst headerRowCount =\n\t\tconfig.headerRowCount ?? (config.headerRows ? config.headerRows.length : 1)\n\n\treturn { ws, totalRows, headerRowCount }\n}\n","import XLSX from \"xlsx\"\n\nimport type { ColumnConfig } from \"./types\"\n\ninterface ApplyProps {\n\tws: XLSX.WorkSheet\n\tcolumns: ColumnConfig[]\n\ttotalRows: number\n\theaderRowCount: number\n}\n\nexport function applyColumnConfig({\n\tws,\n\tcolumns,\n\ttotalRows,\n\theaderRowCount,\n}: ApplyProps) {\n\tws[\"!cols\"] = columns.map((col) => ({ wpx: col.width ?? 100 }))\n\n\tfor (let colIndex = 0; colIndex < columns.length; colIndex++) {\n\t\tconst col = columns[colIndex]\n\t\tif (!col) continue\n\t\tif (!col.type && !col.format) continue\n\n\t\tconst colLetter = XLSX.utils.encode_col(colIndex)\n\n\t\tfor (let row = headerRowCount; row < totalRows; row++) {\n\t\t\tconst cellAddress = `${colLetter}${row + 1}`\n\t\t\tconst cell = ws[cellAddress]\n\t\t\tif (!cell) continue\n\n\t\t\tswitch (col.type) {\n\t\t\t\tcase \"number\":\n\t\t\t\t\tif (typeof cell.v === \"number\") {\n\t\t\t\t\t\tcell.t = \"n\"\n\t\t\t\t\t\tcell.z = col.format || \"#,##0\"\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase \"date\": {\n\t\t\t\t\tconst dt = new Date(cell.v)\n\t\t\t\t\tif (!Number.isNaN(dt.getTime())) {\n\t\t\t\t\t\tcell.t = \"d\"\n\t\t\t\t\t\tcell.v = dt\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tdefault:\n\t\t\t\t\tcell.t = \"s\"\n\t\t\t}\n\n\t\t\tif (col.format) {\n\t\t\t\tcell.z = col.format\n\t\t\t}\n\t\t}\n\t}\n}\n","import XLSX from \"xlsx\"\n\nexport function writeToBlob(ws: XLSX.WorkSheet, sheetName: string) {\n\tconst wb = XLSX.utils.book_new()\n\tXLSX.utils.book_append_sheet(wb, ws, sheetName)\n\n\tconst wbout = XLSX.write(wb, { bookType: \"xlsx\", type: \"array\" })\n\n\treturn new Blob([wbout], {\n\t\ttype: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\t})\n}\n","import XLSX from \"xlsx\"\n\nimport { buildSheet } from \"./builder\"\nimport { applyColumnConfig } from \"./formatter\"\nimport type { ExcelExportConfig } from \"./types\"\nimport { writeToBlob } from \"./writer\"\n\n/**\n * Generate an Excel file (XLSX) from a declarative configuration.\n *\n * This function builds a worksheet using the provided header, data, and row mapping\n * defined in the configuration object. Each column can define metadata such as\n * data type (string/number/date), column width, and optional custom formatting.\n * Once the worksheet is constructed, the function returns a downloadable XLSX `Blob`.\n *\n * ! Environment Constraint:\n * - This function must be executed in a server-side environment\n * as `xlsx` is not browser-compatible.\n * - The output is a `Blob` that can safely be sent to the client for download.\n *\n * Guarantees:\n * - No global state is modified.\n * - No external I/O is performed beyond internal workbook operations.\n * - All XLSX logic is fully encapsulated; the caller only provides configuration.\n *\n * @typeParam T - The type of each item in the `data` array.\n *\n * @param config - Excel export configuration.\n * - `sheetName`: The name of the worksheet.\n * - `header`: Column order for the first row of the Excel file.\n * - `columns`: Column metadata (width/type/format).\n * - `data`: Raw data to be mapped into Excel rows.\n * - `mapRow`: A function that maps each data item into an array of cell values (aligned with the header).\n *\n * @returns A downloadable Excel `Blob`.\n *\n * @example\n * const blob = await exportToExcel({\n * sheetName: \"Master Banner\",\n * header: [\"No\", \"Title\", \"Status\"],\n * columns: [\n * { width: 35, type: \"number\" },\n * { width: 300, type: \"string\" },\n * { width: 100, type: \"string\" },\n * ],\n * data: banners,\n * mapRow: (item, idx) => [idx + 1, item.title, item.active ? \"Active\" : \"Inactive\"],\n * })\n *\n * // on the client\n * const url = URL.createObjectURL(blob)\n * downloadFile(url, \"master-banner.xlsx\")\n */\nexport function exportToExcel<T = unknown>(config: ExcelExportConfig<T>) {\n\tconst { ws, totalRows, headerRowCount } = buildSheet(config)\n\n\tapplyColumnConfig({\n\t\tws,\n\t\tcolumns: config.columns,\n\t\ttotalRows,\n\t\theaderRowCount,\n\t})\n\n\treturn writeToBlob(ws, config.sheetName)\n}\n\nexport async function readExcelToJson<T>(file: File): Promise<T[]> {\n\tconst arrayBuffer = await file.arrayBuffer()\n\tconst workbook = XLSX.read(arrayBuffer, { type: \"array\" })\n\tconst sheetName = workbook.SheetNames[0]\n\tif (!sheetName) {\n\t\tthrow new Error(\"The Excel file does not contain any worksheets.\")\n\t}\n\tconst worksheet = workbook.Sheets[sheetName]\n\tif (!worksheet) {\n\t\tthrow new Error(`Worksheet \"${sheetName}\" could not be found.`)\n\t}\n\treturn XLSX.utils.sheet_to_json(worksheet, { defval: \"\" })\n}\n\nexport type { ExcelExportConfig }\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ExcelExportConfig } from './types.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate an Excel file (XLSX) from a declarative configuration.
|
|
5
|
+
*
|
|
6
|
+
* This function builds a worksheet using the provided header, data, and row mapping
|
|
7
|
+
* defined in the configuration object. Each column can define metadata such as
|
|
8
|
+
* data type (string/number/date), column width, and optional custom formatting.
|
|
9
|
+
* Once the worksheet is constructed, the function returns a downloadable XLSX `Blob`.
|
|
10
|
+
*
|
|
11
|
+
* ! Environment Constraint:
|
|
12
|
+
* - This function must be executed in a server-side environment
|
|
13
|
+
* as `xlsx` is not browser-compatible.
|
|
14
|
+
* - The output is a `Blob` that can safely be sent to the client for download.
|
|
15
|
+
*
|
|
16
|
+
* Guarantees:
|
|
17
|
+
* - No global state is modified.
|
|
18
|
+
* - No external I/O is performed beyond internal workbook operations.
|
|
19
|
+
* - All XLSX logic is fully encapsulated; the caller only provides configuration.
|
|
20
|
+
*
|
|
21
|
+
* @typeParam T - The type of each item in the `data` array.
|
|
22
|
+
*
|
|
23
|
+
* @param config - Excel export configuration.
|
|
24
|
+
* - `sheetName`: The name of the worksheet.
|
|
25
|
+
* - `header`: Column order for the first row of the Excel file.
|
|
26
|
+
* - `columns`: Column metadata (width/type/format).
|
|
27
|
+
* - `data`: Raw data to be mapped into Excel rows.
|
|
28
|
+
* - `mapRow`: A function that maps each data item into an array of cell values (aligned with the header).
|
|
29
|
+
*
|
|
30
|
+
* @returns A downloadable Excel `Blob`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const blob = await exportToExcel({
|
|
34
|
+
* sheetName: "Master Banner",
|
|
35
|
+
* header: ["No", "Title", "Status"],
|
|
36
|
+
* columns: [
|
|
37
|
+
* { width: 35, type: "number" },
|
|
38
|
+
* { width: 300, type: "string" },
|
|
39
|
+
* { width: 100, type: "string" },
|
|
40
|
+
* ],
|
|
41
|
+
* data: banners,
|
|
42
|
+
* mapRow: (item, idx) => [idx + 1, item.title, item.active ? "Active" : "Inactive"],
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* // on the client
|
|
46
|
+
* const url = URL.createObjectURL(blob)
|
|
47
|
+
* downloadFile(url, "master-banner.xlsx")
|
|
48
|
+
*/
|
|
49
|
+
declare function exportToExcel<T = unknown>(config: ExcelExportConfig<T>): Blob;
|
|
50
|
+
declare function readExcelToJson<T>(file: File): Promise<T[]>;
|
|
51
|
+
|
|
52
|
+
export { ExcelExportConfig, exportToExcel, readExcelToJson };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ExcelExportConfig } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate an Excel file (XLSX) from a declarative configuration.
|
|
5
|
+
*
|
|
6
|
+
* This function builds a worksheet using the provided header, data, and row mapping
|
|
7
|
+
* defined in the configuration object. Each column can define metadata such as
|
|
8
|
+
* data type (string/number/date), column width, and optional custom formatting.
|
|
9
|
+
* Once the worksheet is constructed, the function returns a downloadable XLSX `Blob`.
|
|
10
|
+
*
|
|
11
|
+
* ! Environment Constraint:
|
|
12
|
+
* - This function must be executed in a server-side environment
|
|
13
|
+
* as `xlsx` is not browser-compatible.
|
|
14
|
+
* - The output is a `Blob` that can safely be sent to the client for download.
|
|
15
|
+
*
|
|
16
|
+
* Guarantees:
|
|
17
|
+
* - No global state is modified.
|
|
18
|
+
* - No external I/O is performed beyond internal workbook operations.
|
|
19
|
+
* - All XLSX logic is fully encapsulated; the caller only provides configuration.
|
|
20
|
+
*
|
|
21
|
+
* @typeParam T - The type of each item in the `data` array.
|
|
22
|
+
*
|
|
23
|
+
* @param config - Excel export configuration.
|
|
24
|
+
* - `sheetName`: The name of the worksheet.
|
|
25
|
+
* - `header`: Column order for the first row of the Excel file.
|
|
26
|
+
* - `columns`: Column metadata (width/type/format).
|
|
27
|
+
* - `data`: Raw data to be mapped into Excel rows.
|
|
28
|
+
* - `mapRow`: A function that maps each data item into an array of cell values (aligned with the header).
|
|
29
|
+
*
|
|
30
|
+
* @returns A downloadable Excel `Blob`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const blob = await exportToExcel({
|
|
34
|
+
* sheetName: "Master Banner",
|
|
35
|
+
* header: ["No", "Title", "Status"],
|
|
36
|
+
* columns: [
|
|
37
|
+
* { width: 35, type: "number" },
|
|
38
|
+
* { width: 300, type: "string" },
|
|
39
|
+
* { width: 100, type: "string" },
|
|
40
|
+
* ],
|
|
41
|
+
* data: banners,
|
|
42
|
+
* mapRow: (item, idx) => [idx + 1, item.title, item.active ? "Active" : "Inactive"],
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* // on the client
|
|
46
|
+
* const url = URL.createObjectURL(blob)
|
|
47
|
+
* downloadFile(url, "master-banner.xlsx")
|
|
48
|
+
*/
|
|
49
|
+
declare function exportToExcel<T = unknown>(config: ExcelExportConfig<T>): Blob;
|
|
50
|
+
declare function readExcelToJson<T>(file: File): Promise<T[]>;
|
|
51
|
+
|
|
52
|
+
export { ExcelExportConfig, exportToExcel, readExcelToJson };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import XLSX3 from 'xlsx';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
function buildSheet(config) {
|
|
5
|
+
const { header, headerRows, data, mapRow, footerRows } = config;
|
|
6
|
+
const rows = [];
|
|
7
|
+
if (headerRows && headerRows.length > 0) {
|
|
8
|
+
for (let i = 0; i < headerRows.length; i++) {
|
|
9
|
+
const row = headerRows[i];
|
|
10
|
+
if (row) rows.push(row);
|
|
11
|
+
}
|
|
12
|
+
} else if (header && header.length > 0) {
|
|
13
|
+
rows.push(header);
|
|
14
|
+
}
|
|
15
|
+
for (let i = 0; i < data.length; i++) {
|
|
16
|
+
const item = data[i];
|
|
17
|
+
if (item) rows.push(mapRow(item, i));
|
|
18
|
+
}
|
|
19
|
+
if (footerRows && footerRows.length > 0) {
|
|
20
|
+
for (let i = 0; i < footerRows.length; i++) {
|
|
21
|
+
const footerRow = footerRows[i];
|
|
22
|
+
if (footerRow) rows.push(footerRow);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const ws = XLSX3.utils.aoa_to_sheet(rows);
|
|
26
|
+
if (config.merges?.length) {
|
|
27
|
+
ws["!merges"] = config.merges;
|
|
28
|
+
}
|
|
29
|
+
const totalRows = rows.length;
|
|
30
|
+
const headerRowCount = config.headerRowCount ?? (config.headerRows ? config.headerRows.length : 1);
|
|
31
|
+
return { ws, totalRows, headerRowCount };
|
|
32
|
+
}
|
|
33
|
+
function applyColumnConfig({
|
|
34
|
+
ws,
|
|
35
|
+
columns,
|
|
36
|
+
totalRows,
|
|
37
|
+
headerRowCount
|
|
38
|
+
}) {
|
|
39
|
+
ws["!cols"] = columns.map((col) => ({ wpx: col.width ?? 100 }));
|
|
40
|
+
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
|
|
41
|
+
const col = columns[colIndex];
|
|
42
|
+
if (!col) continue;
|
|
43
|
+
if (!col.type && !col.format) continue;
|
|
44
|
+
const colLetter = XLSX3.utils.encode_col(colIndex);
|
|
45
|
+
for (let row = headerRowCount; row < totalRows; row++) {
|
|
46
|
+
const cellAddress = `${colLetter}${row + 1}`;
|
|
47
|
+
const cell = ws[cellAddress];
|
|
48
|
+
if (!cell) continue;
|
|
49
|
+
switch (col.type) {
|
|
50
|
+
case "number":
|
|
51
|
+
if (typeof cell.v === "number") {
|
|
52
|
+
cell.t = "n";
|
|
53
|
+
cell.z = col.format || "#,##0";
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case "date": {
|
|
57
|
+
const dt = new Date(cell.v);
|
|
58
|
+
if (!Number.isNaN(dt.getTime())) {
|
|
59
|
+
cell.t = "d";
|
|
60
|
+
cell.v = dt;
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
default:
|
|
65
|
+
cell.t = "s";
|
|
66
|
+
}
|
|
67
|
+
if (col.format) {
|
|
68
|
+
cell.z = col.format;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function writeToBlob(ws, sheetName) {
|
|
74
|
+
const wb = XLSX3.utils.book_new();
|
|
75
|
+
XLSX3.utils.book_append_sheet(wb, ws, sheetName);
|
|
76
|
+
const wbout = XLSX3.write(wb, { bookType: "xlsx", type: "array" });
|
|
77
|
+
return new Blob([wbout], {
|
|
78
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/index.ts
|
|
83
|
+
function exportToExcel(config) {
|
|
84
|
+
const { ws, totalRows, headerRowCount } = buildSheet(config);
|
|
85
|
+
applyColumnConfig({
|
|
86
|
+
ws,
|
|
87
|
+
columns: config.columns,
|
|
88
|
+
totalRows,
|
|
89
|
+
headerRowCount
|
|
90
|
+
});
|
|
91
|
+
return writeToBlob(ws, config.sheetName);
|
|
92
|
+
}
|
|
93
|
+
async function readExcelToJson(file) {
|
|
94
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
95
|
+
const workbook = XLSX3.read(arrayBuffer, { type: "array" });
|
|
96
|
+
const sheetName = workbook.SheetNames[0];
|
|
97
|
+
if (!sheetName) {
|
|
98
|
+
throw new Error("The Excel file does not contain any worksheets.");
|
|
99
|
+
}
|
|
100
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
101
|
+
if (!worksheet) {
|
|
102
|
+
throw new Error(`Worksheet "${sheetName}" could not be found.`);
|
|
103
|
+
}
|
|
104
|
+
return XLSX3.utils.sheet_to_json(worksheet, { defval: "" });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { exportToExcel, readExcelToJson };
|
|
108
|
+
//# sourceMappingURL=index.js.map
|
|
109
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/builder.ts","../src/formatter.ts","../src/writer.ts","../src/index.ts"],"names":["XLSX"],"mappings":"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","file":"index.js","sourcesContent":["import XLSX from \"xlsx\"\n\nimport type { ExcelExportConfig } from \"./types\"\n\nexport function buildSheet<T>(config: ExcelExportConfig<T>) {\n\tconst { header, headerRows, data, mapRow, footerRows } = config\n\n\tconst rows: (string | number | Date | null)[][] = []\n\n\tif (headerRows && headerRows.length > 0) {\n\t\tfor (let i = 0; i < headerRows.length; i++) {\n\t\t\tconst row = headerRows[i]\n\t\t\tif (row) rows.push(row)\n\t\t}\n\t} else if (header && header.length > 0) {\n\t\trows.push(header)\n\t}\n\n\tfor (let i = 0; i < data.length; i++) {\n\t\tconst item = data[i]\n\t\tif (item) rows.push(mapRow(item, i))\n\t}\n\n\tif (footerRows && footerRows.length > 0) {\n\t\tfor (let i = 0; i < footerRows.length; i++) {\n\t\t\tconst footerRow = footerRows[i]\n\t\t\tif (footerRow) rows.push(footerRow)\n\t\t}\n\t}\n\n\tconst ws = XLSX.utils.aoa_to_sheet(rows)\n\n\tif (config.merges?.length) {\n\t\tws[\"!merges\"] = config.merges\n\t}\n\n\tconst totalRows = rows.length\n\n\tconst headerRowCount =\n\t\tconfig.headerRowCount ?? (config.headerRows ? config.headerRows.length : 1)\n\n\treturn { ws, totalRows, headerRowCount }\n}\n","import XLSX from \"xlsx\"\n\nimport type { ColumnConfig } from \"./types\"\n\ninterface ApplyProps {\n\tws: XLSX.WorkSheet\n\tcolumns: ColumnConfig[]\n\ttotalRows: number\n\theaderRowCount: number\n}\n\nexport function applyColumnConfig({\n\tws,\n\tcolumns,\n\ttotalRows,\n\theaderRowCount,\n}: ApplyProps) {\n\tws[\"!cols\"] = columns.map((col) => ({ wpx: col.width ?? 100 }))\n\n\tfor (let colIndex = 0; colIndex < columns.length; colIndex++) {\n\t\tconst col = columns[colIndex]\n\t\tif (!col) continue\n\t\tif (!col.type && !col.format) continue\n\n\t\tconst colLetter = XLSX.utils.encode_col(colIndex)\n\n\t\tfor (let row = headerRowCount; row < totalRows; row++) {\n\t\t\tconst cellAddress = `${colLetter}${row + 1}`\n\t\t\tconst cell = ws[cellAddress]\n\t\t\tif (!cell) continue\n\n\t\t\tswitch (col.type) {\n\t\t\t\tcase \"number\":\n\t\t\t\t\tif (typeof cell.v === \"number\") {\n\t\t\t\t\t\tcell.t = \"n\"\n\t\t\t\t\t\tcell.z = col.format || \"#,##0\"\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase \"date\": {\n\t\t\t\t\tconst dt = new Date(cell.v)\n\t\t\t\t\tif (!Number.isNaN(dt.getTime())) {\n\t\t\t\t\t\tcell.t = \"d\"\n\t\t\t\t\t\tcell.v = dt\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tdefault:\n\t\t\t\t\tcell.t = \"s\"\n\t\t\t}\n\n\t\t\tif (col.format) {\n\t\t\t\tcell.z = col.format\n\t\t\t}\n\t\t}\n\t}\n}\n","import XLSX from \"xlsx\"\n\nexport function writeToBlob(ws: XLSX.WorkSheet, sheetName: string) {\n\tconst wb = XLSX.utils.book_new()\n\tXLSX.utils.book_append_sheet(wb, ws, sheetName)\n\n\tconst wbout = XLSX.write(wb, { bookType: \"xlsx\", type: \"array\" })\n\n\treturn new Blob([wbout], {\n\t\ttype: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\t})\n}\n","import XLSX from \"xlsx\"\n\nimport { buildSheet } from \"./builder\"\nimport { applyColumnConfig } from \"./formatter\"\nimport type { ExcelExportConfig } from \"./types\"\nimport { writeToBlob } from \"./writer\"\n\n/**\n * Generate an Excel file (XLSX) from a declarative configuration.\n *\n * This function builds a worksheet using the provided header, data, and row mapping\n * defined in the configuration object. Each column can define metadata such as\n * data type (string/number/date), column width, and optional custom formatting.\n * Once the worksheet is constructed, the function returns a downloadable XLSX `Blob`.\n *\n * ! Environment Constraint:\n * - This function must be executed in a server-side environment\n * as `xlsx` is not browser-compatible.\n * - The output is a `Blob` that can safely be sent to the client for download.\n *\n * Guarantees:\n * - No global state is modified.\n * - No external I/O is performed beyond internal workbook operations.\n * - All XLSX logic is fully encapsulated; the caller only provides configuration.\n *\n * @typeParam T - The type of each item in the `data` array.\n *\n * @param config - Excel export configuration.\n * - `sheetName`: The name of the worksheet.\n * - `header`: Column order for the first row of the Excel file.\n * - `columns`: Column metadata (width/type/format).\n * - `data`: Raw data to be mapped into Excel rows.\n * - `mapRow`: A function that maps each data item into an array of cell values (aligned with the header).\n *\n * @returns A downloadable Excel `Blob`.\n *\n * @example\n * const blob = await exportToExcel({\n * sheetName: \"Master Banner\",\n * header: [\"No\", \"Title\", \"Status\"],\n * columns: [\n * { width: 35, type: \"number\" },\n * { width: 300, type: \"string\" },\n * { width: 100, type: \"string\" },\n * ],\n * data: banners,\n * mapRow: (item, idx) => [idx + 1, item.title, item.active ? \"Active\" : \"Inactive\"],\n * })\n *\n * // on the client\n * const url = URL.createObjectURL(blob)\n * downloadFile(url, \"master-banner.xlsx\")\n */\nexport function exportToExcel<T = unknown>(config: ExcelExportConfig<T>) {\n\tconst { ws, totalRows, headerRowCount } = buildSheet(config)\n\n\tapplyColumnConfig({\n\t\tws,\n\t\tcolumns: config.columns,\n\t\ttotalRows,\n\t\theaderRowCount,\n\t})\n\n\treturn writeToBlob(ws, config.sheetName)\n}\n\nexport async function readExcelToJson<T>(file: File): Promise<T[]> {\n\tconst arrayBuffer = await file.arrayBuffer()\n\tconst workbook = XLSX.read(arrayBuffer, { type: \"array\" })\n\tconst sheetName = workbook.SheetNames[0]\n\tif (!sheetName) {\n\t\tthrow new Error(\"The Excel file does not contain any worksheets.\")\n\t}\n\tconst worksheet = workbook.Sheets[sheetName]\n\tif (!worksheet) {\n\t\tthrow new Error(`Worksheet \"${sheetName}\" could not be found.`)\n\t}\n\treturn XLSX.utils.sheet_to_json(worksheet, { defval: \"\" })\n}\n\nexport type { ExcelExportConfig }\n"]}
|
package/dist/types.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"types.cjs"}
|
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface CellMergeRange {
|
|
2
|
+
s: {
|
|
3
|
+
r: number;
|
|
4
|
+
c: number;
|
|
5
|
+
};
|
|
6
|
+
e: {
|
|
7
|
+
r: number;
|
|
8
|
+
c: number;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
interface ColumnConfig {
|
|
12
|
+
width?: number;
|
|
13
|
+
type?: "string" | "number" | "date";
|
|
14
|
+
format?: string;
|
|
15
|
+
}
|
|
16
|
+
interface ExcelExportConfig<T = unknown> {
|
|
17
|
+
sheetName: string;
|
|
18
|
+
header?: string[];
|
|
19
|
+
headerRows?: (string | null)[][];
|
|
20
|
+
footerRows?: (string | number | Date | null)[][];
|
|
21
|
+
headerRowCount?: number;
|
|
22
|
+
columns: ColumnConfig[];
|
|
23
|
+
data: T[];
|
|
24
|
+
mapRow: (item: T, index: number) => (string | number | Date | null)[];
|
|
25
|
+
merges?: CellMergeRange[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type { CellMergeRange, ColumnConfig, ExcelExportConfig };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface CellMergeRange {
|
|
2
|
+
s: {
|
|
3
|
+
r: number;
|
|
4
|
+
c: number;
|
|
5
|
+
};
|
|
6
|
+
e: {
|
|
7
|
+
r: number;
|
|
8
|
+
c: number;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
interface ColumnConfig {
|
|
12
|
+
width?: number;
|
|
13
|
+
type?: "string" | "number" | "date";
|
|
14
|
+
format?: string;
|
|
15
|
+
}
|
|
16
|
+
interface ExcelExportConfig<T = unknown> {
|
|
17
|
+
sheetName: string;
|
|
18
|
+
header?: string[];
|
|
19
|
+
headerRows?: (string | null)[][];
|
|
20
|
+
footerRows?: (string | number | Date | null)[][];
|
|
21
|
+
headerRowCount?: number;
|
|
22
|
+
columns: ColumnConfig[];
|
|
23
|
+
data: T[];
|
|
24
|
+
mapRow: (item: T, index: number) => (string | number | Date | null)[];
|
|
25
|
+
merges?: CellMergeRange[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type { CellMergeRange, ColumnConfig, ExcelExportConfig };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"types.js"}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farhantallei/excel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Declarative and type-safe Excel (XLSX) export for Node.js.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"excel",
|
|
8
|
+
"xlsx",
|
|
9
|
+
"excel-export",
|
|
10
|
+
"xlsx-export",
|
|
11
|
+
"typescript",
|
|
12
|
+
"type-safe",
|
|
13
|
+
"nodejs",
|
|
14
|
+
"server-only",
|
|
15
|
+
"spreadsheet",
|
|
16
|
+
"excel-generator",
|
|
17
|
+
"excel-builder",
|
|
18
|
+
"data-export",
|
|
19
|
+
"tabular-data"
|
|
20
|
+
],
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Farhan Pradana Tallei",
|
|
23
|
+
"email": "farhan.pradana55@gmail.com"
|
|
24
|
+
},
|
|
25
|
+
"module": "dist/index.js",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup --config tsup.config.ts",
|
|
30
|
+
"lint": "biome check",
|
|
31
|
+
"format": "biome check --write --unsafe",
|
|
32
|
+
"publint": "publint",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"prepare": "bun run build"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"registry": "https://registry.npmjs.org/",
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/farhantallei/excel",
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"import": {
|
|
48
|
+
"types": "./dist/index.d.ts",
|
|
49
|
+
"default": "./dist/index.js"
|
|
50
|
+
},
|
|
51
|
+
"require": {
|
|
52
|
+
"types": "./dist/index.d.cts",
|
|
53
|
+
"default": "./dist/index.cjs"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"./types": {
|
|
57
|
+
"types": "./dist/types.d.ts"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@biomejs/biome": "2.3.15",
|
|
62
|
+
"@types/bun": "latest",
|
|
63
|
+
"publint": "^0.3.17",
|
|
64
|
+
"tsup": "^8.5.1"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"typescript": "^5"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"xlsx": "^0.18.5"
|
|
71
|
+
}
|
|
72
|
+
}
|