Skip to content

Commit

Permalink
feat: add cli & binaries
Browse files Browse the repository at this point in the history
chore: wip

chore: wip
  • Loading branch information
chrisbbreuer committed Dec 20, 2024
1 parent 96146a1 commit 04feded
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 23 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,15 @@ jobs:
run: bunx changelogithub
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

- name: Attach Binaries
uses: softprops/action-gh-release@v2
with:
files: |
bin/spreadsheets-linux-x64
bin/spreadsheets-linux-arm64
bin/spreadsheets-windows-x64.exe
bin/spreadsheets-darwin-x64
bin/spreadsheets-darwin-arm64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ logs
node_modules
temp
docs/.vitepress/cache
bin/spreadsheets
invalid.json
1 change: 1 addition & 0 deletions .vscode/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ preinstall
quickfix
shikijs
socio
softprops
Solana
Spatie
stacksjs
Expand Down
134 changes: 134 additions & 0 deletions bin/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env bun
import { readFileSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import process from 'node:process'
import { CAC } from 'cac'
import { type Content, createSpreadsheet, csvToContent } from '../src/index'

// Use sync version for version to avoid race conditions
const version = process.env.NODE_ENV === 'test'
? '0.0.0'
: JSON.parse(
readFileSync(resolve(__dirname, '../package.json'), 'utf-8'),
).version

interface CLIOptions {
type?: 'csv' | 'excel'
output?: string
pretty?: boolean
}

const cli = new CAC('spreadsheets')

function logMessage(message: string) {
if (process.env.NODE_ENV === 'test') {
process.stderr.write(`${message}\n`)
}
else {
console.log(message)
}
}

cli
.command('create <input>', 'Create a spreadsheet from JSON input file')
.option('-t, --type <type>', 'Output type (csv or excel)', { default: 'csv' })
.option('-o, --output <path>', 'Output file path')
.action(async (input: string, options: CLIOptions) => {
try {
const inputPath = resolve(process.cwd(), input)
const content = JSON.parse(await readFile(inputPath, 'utf-8')) as Content

const result = createSpreadsheet(content, { type: options.type })

if (options.output) {
await result.store(options.output)
logMessage(`Spreadsheet saved to ${options.output}`)
}
else {
const output = result.getContent()
if (typeof output === 'string') {
process.stdout.write(output)
}
else {
process.stdout.write(output)
}
}
}
catch (error) {
logMessage(`Failed to create spreadsheet: ${(error as Error).message}`)
process.exit(1)
}
})

cli
.command('convert <input> <output>', 'Convert between spreadsheet formats')
.action(async (input: string, output: string) => {
try {
const inputExt = input.slice(input.lastIndexOf('.')) as '.csv' | '.xlsx'
const outputExt = output.slice(output.lastIndexOf('.')) as '.csv' | '.xlsx'

if (inputExt === outputExt) {
logMessage('Input and output formats are the same')
return
}

// Handle CSV input
let content: Content
if (inputExt === '.csv') {
content = await csvToContent(input)
}
else {
// Handle JSON input
content = JSON.parse(await readFile(input, 'utf-8')) as Content
}

const outputType = outputExt === '.csv' ? 'csv' : 'excel'
const result = createSpreadsheet(content, { type: outputType })
await result.store(output)

logMessage(`Converted ${input} to ${output}`)
}
catch (error) {
logMessage(`Failed to convert spreadsheet: ${(error as Error).message}`)
process.exit(1)
}
})

cli
.command('validate <input>', 'Validate JSON input format')
.action(async (input: string) => {
try {
const content = JSON.parse(await readFile(input, 'utf-8'))

if (!content.headings || !Array.isArray(content.headings)) {
throw new Error('Missing or invalid headings array')
}
if (!content.data || !Array.isArray(content.data)) {
throw new Error('Missing or invalid data array')
}

const invalidHeadings = content.headings.some((h: unknown) => typeof h !== 'string')
if (invalidHeadings) {
throw new Error('Headings must be strings')
}

const invalidData = content.data.some((row: unknown[]) =>
!Array.isArray(row)
|| row.some((cell: unknown) => typeof cell !== 'string' && typeof cell !== 'number'),
)
if (invalidData) {
throw new Error('Data must be an array of arrays containing only strings and numbers')
}

logMessage('Input JSON is valid')
}
catch (error) {
logMessage(`Invalid input: ${(error as Error).message}`)
process.exit(1)
}
})

cli.version(version)
cli.help()
cli.parse()
29 changes: 25 additions & 4 deletions build.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
import process from 'node:process'
import { dts } from 'bun-plugin-dtsx'

await Bun.build({
entrypoints: ['src/index.ts'],
console.log('Building...')

const result = await Bun.build({
entrypoints: ['./src/index.ts', './bin/cli.ts'],
outdir: './dist',
target: 'bun',
plugins: [dts()],
format: 'esm',
target: 'node',
minify: true,
splitting: true,
plugins: [
dts(),
],
})

if (!result.success) {
console.error('Build failed')

for (const message of result.logs) {
// Bun will pretty print the message object
console.error(message)
}

process.exit(1)
}

console.log('Build complete')
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,27 @@
},
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"spreadsheets": "./dist/bin/cli.js"
},
"files": [
"dist",
"src"
],
"scripts": {
"build": "bun --bun build.ts",
"build": "bun build.ts && bun run compile",
"compile": "bun build ./bin/cli.ts --compile --minify --outfile bin/spreadsheets",
"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",
"compile:linux-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-x64 --outfile bin/spreadsheets-linux-x64",
"compile:linux-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-arm64 --outfile bin/spreadsheets-linux-arm64",
"compile:windows-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-windows-x64 --outfile bin/spreadsheets-windows-x64.exe",
"compile:darwin-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-x64 --outfile bin/spreadsheets-darwin-x64",
"compile:darwin-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-arm64 --outfile bin/spreadsheets-darwin-arm64",
"lint": "bunx --bun eslint --flag unstable_ts_config .",
"lint:fix": "bunx --bun eslint --flag unstable_ts_config . --fix",
"fresh": "bunx rimraf node_modules/ bun.lock && bun i",
"changelog": "bunx changelogen --output CHANGELOG.md",
"prepublishOnly": "bun --bun run build",
"prepublishOnly": "bun --bun run build && bun run compile:all",
"release": "bun run changelog && bunx bumpp package.json --all",
"test": "bun test",
"dev:docs": "bun --bun vitepress dev docs",
Expand Down
2 changes: 2 additions & 0 deletions spreadsheets
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env bun
import('./bin/cli')
35 changes: 34 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
SpreadsheetType,
} from './types'
import { Buffer } from 'node:buffer'
import { writeFile } from 'node:fs/promises'
import { readFile, writeFile } from 'node:fs/promises'
import { gzipSync } from 'node:zlib'

export const spreadsheet: Spreadsheet = Object.assign(
Expand Down Expand Up @@ -242,4 +242,37 @@ export function generateExcelContent(content: Content): Uint8Array {
return result
}

export async function csvToContent(csvPath: string): Promise<Content> {
const csvContent = await readFile(csvPath, 'utf-8')

// Split into lines and parse CSV
const lines = csvContent.split('\n').map(line =>
line.split(',').map((cell) => {
const trimmed = cell.trim()
// Remove quotes if present
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
// Handle escaped quotes
return trimmed.slice(1, -1).replace(/""/g, '"')
}
return trimmed
}),
)

// First line is headers
const [headings, ...data] = lines

// Convert numeric strings to numbers
const typedData = data.map(row =>
row.map((cell) => {
const num = Number(cell)
return !Number.isNaN(num) && cell.trim() !== '' ? num : cell
}),
)

return {
headings,
data: typedData,
}
}

export * from './types'
Loading

0 comments on commit 04feded

Please sign in to comment.