Skip to content

Commit 1db707e

Browse files
committed
chore: wip
chore: wip chore: wip chore: wip chore: wip
1 parent 1cf08c1 commit 1db707e

File tree

6 files changed

+381
-0
lines changed

6 files changed

+381
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Bun Spreadsheets
2+
3+
Easily generate spreadsheets, like CSVs and Excel files.
4+
5+
## ☘️ Features
6+
7+
- Generate CSV files
8+
- Generate Excel files
9+
- Store spreadsheets to disk
10+
- Download spreadsheets as a Response object
11+
- Simple API for creating and manipulating spreadsheets
12+
- Fully typed
13+
- Optimized for Bun
14+
- Lightweight & dependency-free
15+
16+
## 🤖 Usage
17+
18+
```bash
19+
bun install bun-spreadsheets
20+
```
21+
22+
Now, you can use it in your project:
23+
24+
```ts
25+
import { createSpreadsheet } from 'bun-spreadsheets'
26+
27+
// Create a spreadsheet
28+
const data = {
29+
headings: ['Name', 'Age', 'City'],
30+
data: [
31+
['John Doe', '30', 'New York'],
32+
['Jane Smith', '25', 'London'],
33+
['Bob Johnson', '35', 'Paris']
34+
]
35+
}
36+
37+
// Generate a CSV spreadsheet
38+
const csvSpreadsheet = createSpreadsheet(data, 'csv')
39+
40+
// Generate an Excel spreadsheet
41+
const excelSpreadsheet = createSpreadsheet(data, 'excel')
42+
43+
// Store the spreadsheet to disk
44+
await spreadsheet.store(csvSpreadsheet, 'output.csv')
45+
46+
// Create a download response
47+
const response = spreadsheet.download(excelSpreadsheet, 'data.xlsx')
48+
```
49+
50+
To view the full documentation, please visit [https://stacksjs.org/docs/bun-spreadsheets](https://stacksjs.org/docs/bun-spreadsheets).
51+
52+
## 🧪 Testing
53+
54+
```bash
55+
bun test
56+
```
57+
58+
## 📈 Changelog
59+
60+
Please see our [releases](https://github.com/stacksjs/stacks/releases) page for more information on what has changed recently.
61+
62+
## 🚜 Contributing
63+
64+
Please review the [Contributing Guide](https://github.com/stacksjs/contributing) for details.
65+
66+
## 🏝 Community
67+
68+
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
69+
70+
[Discussions on GitHub](https://github.com/stacksjs/stacks/discussions)
71+
72+
For casual chit-chat with others using this package:
73+
74+
[Join the Stacks Discord Server](https://discord.gg/stacksjs)
75+
76+
## 🙏🏼 Credits
77+
78+
Many thanks to the following core technologies & people who have contributed to this package:
79+
80+
- [Chris Breuer](https://github.com/chrisbbreuer)
81+
- [All Contributors](../../contributors)
82+
83+
## 📄 License
84+
85+
The MIT License (MIT). Please see [LICENSE](https://github.com/stacksjs/stacks/tree/main/LICENSE.md) for more information.
86+
87+
Made with 💙
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { intro, outro } from '../build/src'
2+
3+
const { startTime } = await intro({
4+
dir: import.meta.dir,
5+
})
6+
7+
const result = await Bun.build({
8+
entrypoints: ['./src/index.ts'],
9+
outdir: './dist',
10+
format: 'esm',
11+
target: 'bun',
12+
sourcemap: 'linked',
13+
minify: true,
14+
})
15+
16+
await outro({
17+
dir: import.meta.dir,
18+
startTime,
19+
result,
20+
})
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "bun-spreadsheets",
3+
"type": "module",
4+
"version": "0.64.6",
5+
"description": "Easily generate spreadsheets, like CSVs and Excel files.",
6+
"author": "Chris Breuer",
7+
"license": "MIT",
8+
"funding": "https://github.com/sponsors/chrisbbreuer",
9+
"homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/spreadsheets#readme",
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/stacksjs/stacks.git",
13+
"directory": "./storage/framework/core/spreadsheets"
14+
},
15+
"bugs": {
16+
"url": "https://github.com/stacksjs/stacks/issues"
17+
},
18+
"keywords": [
19+
"spreadsheet",
20+
"csv",
21+
"excel",
22+
"export",
23+
"functional",
24+
"functions",
25+
"bun"
26+
],
27+
"exports": {
28+
".": {
29+
"bun": "./src/index.ts",
30+
"import": "./dist/index.js"
31+
},
32+
"./*": {
33+
"bun": "./src/*",
34+
"import": "./dist/*"
35+
}
36+
},
37+
"module": "dist/index.js",
38+
"types": "dist/index.d.ts",
39+
"contributors": ["Chris Breuer <chris@stacksjs.org>"],
40+
"files": ["README.md", "dist", "src"],
41+
"scripts": {
42+
"build": "bun --bun build.ts",
43+
"typecheck": "bun --bun tsc --noEmit",
44+
"prepublishOnly": "bun run build"
45+
},
46+
"devDependencies": {
47+
"@stacksjs/development": "workspace:*"
48+
}
49+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/**
2+
* Content is the data structure that represents the spreadsheet content.
3+
*
4+
* @example
5+
* const content: Content = {
6+
* headings: ['Name', 'Age', 'City'],
7+
* data: [
8+
* ['John Doe', '30', 'New York'],
9+
* ['Jane Smith', '25', 'London'],
10+
* ['Bob Johnson', '35', 'Paris']
11+
* ]
12+
* }
13+
*/
14+
export interface Content {
15+
headings: string[]
16+
data: string[][]
17+
}
18+
19+
export type SpreadsheetType = 'csv' | 'excel'
20+
21+
export interface SpreadsheetContent {
22+
content: string | Uint8Array
23+
type: SpreadsheetType
24+
}
25+
26+
export interface SpreadsheetOptions {
27+
type: SpreadsheetType
28+
}
29+
30+
export type Spreadsheet = {
31+
create: (data: Content, options: SpreadsheetOptions) => SpreadsheetContent
32+
generate: (data: Content, options: SpreadsheetOptions) => string | Uint8Array
33+
generateCSV: (content: Content) => string | SpreadsheetWrapper
34+
generateExcel: (content: Content) => Uint8Array | SpreadsheetWrapper
35+
store: (spreadsheet: SpreadsheetContent, path: string) => Promise<void>
36+
download: (spreadsheet: SpreadsheetContent, filename: string) => Response
37+
}
38+
39+
export const spreadsheet: Spreadsheet = {
40+
generate: (data: Content, options: SpreadsheetOptions = { type: 'csv' }): string | Uint8Array => {
41+
const generators: Record<SpreadsheetType, (content: Content) => string | Uint8Array | SpreadsheetWrapper> = {
42+
csv: spreadsheet.generateCSV,
43+
excel: spreadsheet.generateExcel,
44+
}
45+
46+
const generator = generators[options.type]
47+
48+
if (!generator) {
49+
throw new Error(`Unsupported spreadsheet type: ${options.type}`)
50+
}
51+
52+
const result = generator(data)
53+
if (result instanceof SpreadsheetWrapper) {
54+
return result.getContent()
55+
}
56+
return result
57+
},
58+
59+
create: (data: Content, options: SpreadsheetOptions = { type: 'csv' }): SpreadsheetContent => ({
60+
content: spreadsheet.generate(data, options),
61+
type: options.type,
62+
}),
63+
64+
generateCSV: (content: Content): string | SpreadsheetWrapper => {
65+
const csvContent = generateCSVContent(content)
66+
return new SpreadsheetWrapper(csvContent, 'csv')
67+
},
68+
69+
generateExcel: (content: Content): Uint8Array | SpreadsheetWrapper => {
70+
const excelContent = generateExcelContent(content)
71+
return new SpreadsheetWrapper(excelContent, 'excel')
72+
},
73+
74+
store: async ({ content }: SpreadsheetContent, path: string): Promise<void> => {
75+
try {
76+
await Bun.write(path, content)
77+
} catch (error) {
78+
throw new Error(`Failed to store spreadsheet: ${(error as Error).message}`)
79+
}
80+
},
81+
82+
download: ({ content, type }: SpreadsheetContent, filename: string): Response => {
83+
const mimeType = type === 'csv' ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
84+
const blob = new Blob([content], { type: mimeType })
85+
86+
return new Response(blob, {
87+
headers: {
88+
'Content-Type': mimeType,
89+
'Content-Disposition': `attachment; filename="${filename}"`,
90+
},
91+
})
92+
},
93+
}
94+
95+
export class SpreadsheetWrapper {
96+
constructor(
97+
private content: string | Uint8Array,
98+
private type: SpreadsheetType,
99+
) {}
100+
101+
getContent(): string | Uint8Array {
102+
return this.content
103+
}
104+
105+
download(filename: string): Response {
106+
return spreadsheet.download({ content: this.content, type: this.type }, filename)
107+
}
108+
109+
store(path: string): Promise<void> {
110+
return spreadsheet.store({ content: this.content, type: this.type }, path)
111+
}
112+
}
113+
114+
export function createSpreadsheet(data: Content, options: SpreadsheetOptions = { type: 'csv' }): SpreadsheetWrapper {
115+
const content = spreadsheet.generate(data, options)
116+
return new SpreadsheetWrapper(content, options.type)
117+
}
118+
119+
export function generateCSVContent(content: Content): string {
120+
const rows = [content.headings, ...content.data]
121+
return rows.map((row) => row.join(',')).join('\n')
122+
}
123+
124+
export function generateExcelContent(content: Content): Uint8Array {
125+
const workbook = Buffer.from(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
126+
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
127+
<sheets>
128+
<sheet name="Sheet1" sheetId="1" r:id="rId1"/>
129+
</sheets>
130+
</workbook>`)
131+
132+
const worksheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
133+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
134+
<sheetData>
135+
${[content.headings, ...content.data]
136+
.map(
137+
(row, rowIndex) => `
138+
<row r="${rowIndex + 1}">
139+
${row
140+
.map(
141+
(cell, cellIndex) => `
142+
<c r="${String.fromCharCode(65 + cellIndex)}${rowIndex + 1}">
143+
<v>${cell}</v>
144+
</c>`,
145+
)
146+
.join('')}
147+
</row>`,
148+
)
149+
.join('')}
150+
</sheetData>
151+
</worksheet>`
152+
153+
const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
154+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
155+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
156+
<Default Extension="xml" ContentType="application/xml"/>
157+
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
158+
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
159+
</Types>`
160+
161+
const rels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
162+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
163+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
164+
</Relationships>`
165+
166+
const files: Array<{ name: string; content: string | Uint8Array }> = [
167+
{ name: '[Content_Types].xml', content: contentTypes },
168+
{ name: '_rels/.rels', content: rels },
169+
{ name: 'xl/workbook.xml', content: workbook },
170+
{ name: 'xl/worksheets/sheet1.xml', content: worksheet },
171+
]
172+
173+
const zipData = files.map((file) => {
174+
const compressedContent = Bun.gzipSync(Buffer.from(file.content))
175+
const header = Buffer.alloc(30 + file.name.length)
176+
header.write('PK\x03\x04', 0)
177+
header.writeUInt32LE(0x0008, 4)
178+
header.writeUInt32LE(compressedContent.length, 18)
179+
header.writeUInt32LE(Buffer.from(file.content).length, 22)
180+
header.writeUInt16LE(file.name.length, 26)
181+
header.write(file.name, 30)
182+
183+
return Buffer.concat([header, compressedContent])
184+
})
185+
186+
const centralDirectory = files.map((file, index) => {
187+
const header = Buffer.alloc(46 + file.name.length)
188+
header.write('PK\x01\x02', 0)
189+
header.writeUInt16LE(0x0014, 4)
190+
header.writeUInt16LE(0x0008, 6)
191+
header.writeUInt32LE(0x0008, 8)
192+
// biome-ignore lint/style/noNonNullAssertion: We know that zipData[index] is not null because we are iterating over files
193+
header.writeUInt32LE(zipData[index]!.length - 30 - file.name.length, 20)
194+
header.writeUInt32LE(
195+
zipData.slice(0, index).reduce((acc, curr) => acc + curr.length, 0),
196+
42,
197+
)
198+
header.writeUInt16LE(file.name.length, 28)
199+
header.write(file.name, 46)
200+
return header
201+
})
202+
203+
const endOfCentralDirectory = Buffer.alloc(22)
204+
endOfCentralDirectory.write('PK\x05\x06', 0)
205+
endOfCentralDirectory.writeUInt16LE(files.length, 8)
206+
endOfCentralDirectory.writeUInt16LE(files.length, 10)
207+
endOfCentralDirectory.writeUInt32LE(
208+
centralDirectory.reduce((acc, curr) => acc + curr.length, 0),
209+
12,
210+
)
211+
endOfCentralDirectory.writeUInt32LE(
212+
zipData.reduce((acc, curr) => acc + curr.length, 0),
213+
16,
214+
)
215+
216+
return Uint8Array.from(Buffer.concat([...zipData, ...centralDirectory, endOfCentralDirectory]))
217+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
describe('example test', () => {
2+
it('assert', () => {
3+
expect(1).toBe(1)
4+
})
5+
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../tsconfig.json"
3+
}

0 commit comments

Comments
 (0)