Skip to content

Commit ddb5577

Browse files
committed
feat: add cli & binaries
chore: wip chore: wip chore: wip
1 parent 96146a1 commit ddb5577

File tree

9 files changed

+462
-23
lines changed

9 files changed

+462
-23
lines changed

.github/workflows/release.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,15 @@ jobs:
3838
run: bunx changelogithub
3939
env:
4040
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
41+
42+
- name: Attach Binaries
43+
uses: softprops/action-gh-release@v2
44+
with:
45+
files: |
46+
bin/spreadsheets-linux-x64
47+
bin/spreadsheets-linux-arm64
48+
bin/spreadsheets-windows-x64.exe
49+
bin/spreadsheets-darwin-x64
50+
bin/spreadsheets-darwin-arm64
51+
env:
52+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ logs
1010
node_modules
1111
temp
1212
docs/.vitepress/cache
13+
bin/spreadsheets
14+
invalid.json

.vscode/dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ preinstall
2828
quickfix
2929
shikijs
3030
socio
31+
softprops
3132
Solana
3233
Spatie
3334
stacksjs

bin/cli.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env bun
2+
import { readFileSync } from 'node:fs'
3+
import { readFile } from 'node:fs/promises'
4+
import { resolve } from 'node:path'
5+
import process from 'node:process'
6+
import { CAC } from 'cac'
7+
import { type Content, createSpreadsheet, csvToContent } from '../src/index'
8+
9+
// Use sync version for version to avoid race conditions
10+
const version = process.env.NODE_ENV === 'test'
11+
? '0.0.0'
12+
: JSON.parse(
13+
readFileSync(resolve(__dirname, '../package.json'), 'utf-8'),
14+
).version
15+
16+
interface CLIOptions {
17+
type?: 'csv' | 'excel'
18+
output?: string
19+
pretty?: boolean
20+
}
21+
22+
const cli = new CAC('spreadsheets')
23+
24+
function logMessage(message: string) {
25+
if (process.env.NODE_ENV === 'test') {
26+
process.stderr.write(`${message}\n`)
27+
}
28+
else {
29+
console.log(message)
30+
}
31+
}
32+
33+
cli
34+
.command('create <input>', 'Create a spreadsheet from JSON input file')
35+
.option('-t, --type <type>', 'Output type (csv or excel)', { default: 'csv' })
36+
.option('-o, --output <path>', 'Output file path')
37+
.action(async (input: string, options: CLIOptions) => {
38+
try {
39+
const inputPath = resolve(process.cwd(), input)
40+
const content = JSON.parse(await readFile(inputPath, 'utf-8')) as Content
41+
42+
const result = createSpreadsheet(content, { type: options.type })
43+
44+
if (options.output) {
45+
await result.store(options.output)
46+
logMessage(`Spreadsheet saved to ${options.output}`)
47+
}
48+
else {
49+
const output = result.getContent()
50+
if (typeof output === 'string') {
51+
process.stdout.write(output)
52+
}
53+
else {
54+
process.stdout.write(output)
55+
}
56+
}
57+
}
58+
catch (error) {
59+
logMessage(`Failed to create spreadsheet: ${(error as Error).message}`)
60+
process.exit(1)
61+
}
62+
})
63+
64+
cli
65+
.command('convert <input> <output>', 'Convert between spreadsheet formats')
66+
.action(async (input: string, output: string) => {
67+
try {
68+
const inputExt = input.slice(input.lastIndexOf('.')) as '.csv' | '.xlsx'
69+
const outputExt = output.slice(output.lastIndexOf('.')) as '.csv' | '.xlsx'
70+
71+
if (inputExt === outputExt) {
72+
logMessage('Input and output formats are the same')
73+
return
74+
}
75+
76+
// Handle CSV input
77+
let content: Content
78+
if (inputExt === '.csv') {
79+
content = await csvToContent(input)
80+
}
81+
else {
82+
// Handle JSON input
83+
content = JSON.parse(await readFile(input, 'utf-8')) as Content
84+
}
85+
86+
const outputType = outputExt === '.csv' ? 'csv' : 'excel'
87+
const result = createSpreadsheet(content, { type: outputType })
88+
await result.store(output)
89+
90+
logMessage(`Converted ${input} to ${output}`)
91+
}
92+
catch (error) {
93+
logMessage(`Failed to convert spreadsheet: ${(error as Error).message}`)
94+
process.exit(1)
95+
}
96+
})
97+
98+
cli
99+
.command('validate <input>', 'Validate JSON input format')
100+
.action(async (input: string) => {
101+
try {
102+
const content = JSON.parse(await readFile(input, 'utf-8'))
103+
104+
if (!content.headings || !Array.isArray(content.headings)) {
105+
throw new Error('Missing or invalid headings array')
106+
}
107+
if (!content.data || !Array.isArray(content.data)) {
108+
throw new Error('Missing or invalid data array')
109+
}
110+
111+
const invalidHeadings = content.headings.some((h: unknown) => typeof h !== 'string')
112+
if (invalidHeadings) {
113+
throw new Error('Headings must be strings')
114+
}
115+
116+
const invalidData = content.data.some((row: unknown[]) =>
117+
!Array.isArray(row)
118+
|| row.some((cell: unknown) => typeof cell !== 'string' && typeof cell !== 'number'),
119+
)
120+
if (invalidData) {
121+
throw new Error('Data must be an array of arrays containing only strings and numbers')
122+
}
123+
124+
logMessage('Input JSON is valid')
125+
}
126+
catch (error) {
127+
logMessage(`Invalid input: ${(error as Error).message}`)
128+
process.exit(1)
129+
}
130+
})
131+
132+
cli.version(version)
133+
cli.help()
134+
cli.parse()

build.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1+
import process from 'node:process'
12
import { dts } from 'bun-plugin-dtsx'
23

3-
await Bun.build({
4-
entrypoints: ['src/index.ts'],
4+
console.log('Building...')
5+
6+
const result = await Bun.build({
7+
entrypoints: ['./src/index.ts', './bin/cli.ts'],
58
outdir: './dist',
6-
target: 'bun',
7-
plugins: [dts()],
9+
format: 'esm',
10+
target: 'node',
11+
minify: true,
12+
splitting: true,
13+
plugins: [
14+
dts(),
15+
],
816
})
17+
18+
if (!result.success) {
19+
console.error('Build failed')
20+
21+
for (const message of result.logs) {
22+
// Bun will pretty print the message object
23+
console.error(message)
24+
}
25+
26+
process.exit(1)
27+
}
28+
29+
console.log('Build complete')

package.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,27 @@
3636
},
3737
"module": "./dist/index.js",
3838
"types": "./dist/index.d.ts",
39+
"bin": {
40+
"spreadsheets": "./dist/bin/cli.js"
41+
},
3942
"files": [
4043
"dist",
4144
"src"
4245
],
4346
"scripts": {
44-
"build": "bun --bun build.ts",
47+
"build": "bun build.ts && bun run compile",
48+
"compile": "bun build ./bin/cli.ts --compile --minify --outfile bin/spreadsheets",
49+
"compile:all": "bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64",
50+
"compile:linux-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-x64 --outfile bin/spreadsheets-linux-x64",
51+
"compile:linux-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-arm64 --outfile bin/spreadsheets-linux-arm64",
52+
"compile:windows-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-windows-x64 --outfile bin/spreadsheets-windows-x64.exe",
53+
"compile:darwin-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-x64 --outfile bin/spreadsheets-darwin-x64",
54+
"compile:darwin-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-arm64 --outfile bin/spreadsheets-darwin-arm64",
4555
"lint": "bunx --bun eslint --flag unstable_ts_config .",
4656
"lint:fix": "bunx --bun eslint --flag unstable_ts_config . --fix",
4757
"fresh": "bunx rimraf node_modules/ bun.lock && bun i",
4858
"changelog": "bunx changelogen --output CHANGELOG.md",
49-
"prepublishOnly": "bun --bun run build",
59+
"prepublishOnly": "bun --bun run build && bun run compile:all",
5060
"release": "bun run changelog && bunx bumpp package.json --all",
5161
"test": "bun test",
5262
"dev:docs": "bun --bun vitepress dev docs",

spreadsheets

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bun
2+
import('./bin/cli')

src/index.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
SpreadsheetType,
88
} from './types'
99
import { Buffer } from 'node:buffer'
10-
import { writeFile } from 'node:fs/promises'
10+
import { readFile, writeFile } from 'node:fs/promises'
1111
import { gzipSync } from 'node:zlib'
1212

1313
export const spreadsheet: Spreadsheet = Object.assign(
@@ -242,4 +242,37 @@ export function generateExcelContent(content: Content): Uint8Array {
242242
return result
243243
}
244244

245+
export async function csvToContent(csvPath: string): Promise<Content> {
246+
const csvContent = await readFile(csvPath, 'utf-8')
247+
248+
// Split into lines and parse CSV
249+
const lines = csvContent.split('\n').map(line =>
250+
line.split(',').map((cell) => {
251+
const trimmed = cell.trim()
252+
// Remove quotes if present
253+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
254+
// Handle escaped quotes
255+
return trimmed.slice(1, -1).replace(/""/g, '"')
256+
}
257+
return trimmed
258+
}),
259+
)
260+
261+
// First line is headers
262+
const [headings, ...data] = lines
263+
264+
// Convert numeric strings to numbers
265+
const typedData = data.map(row =>
266+
row.map((cell) => {
267+
const num = Number(cell)
268+
return !Number.isNaN(num) && cell.trim() !== '' ? num : cell
269+
}),
270+
)
271+
272+
return {
273+
headings,
274+
data: typedData,
275+
}
276+
}
277+
245278
export * from './types'

0 commit comments

Comments
 (0)