436 lines
10 KiB
Markdown
436 lines
10 KiB
Markdown
---
|
|
name: bun-dev
|
|
description: This skill should be used when working with Bun runtime, bun:sqlite, Bun.serve, bun:test, or when "Bun", "bun:test", or Bun-specific patterns are mentioned.
|
|
metadata:
|
|
version: "1.0.0"
|
|
---
|
|
|
|
# Bun Development
|
|
|
|
Bun runtime → native APIs → zero-dependency patterns.
|
|
|
|
<when_to_use>
|
|
|
|
- Bun runtime development
|
|
- SQLite database with bun:sqlite
|
|
- HTTP server with Bun.serve
|
|
- Testing with bun:test
|
|
- File operations with Bun.file/Bun.write
|
|
- Shell operations with $ template
|
|
- Password hashing with Bun.password
|
|
- Environment variable handling
|
|
- Building and bundling
|
|
|
|
NOT for: Node.js-only patterns, cross-runtime libraries, non-Bun projects
|
|
|
|
</when_to_use>
|
|
|
|
<runtime_basics>
|
|
|
|
**Package management**:
|
|
|
|
```bash
|
|
bun install # Install deps
|
|
bun add zod # Add package
|
|
bun remove zod # Remove package
|
|
bun update # Update all
|
|
```
|
|
|
|
**Script execution**:
|
|
|
|
```bash
|
|
bun run dev # Run package.json script
|
|
bun run src/index.ts # Execute TypeScript directly
|
|
bun --watch index.ts # Watch mode
|
|
```
|
|
|
|
**Testing**:
|
|
|
|
```bash
|
|
bun test # All tests
|
|
bun test src/ # Directory
|
|
bun test --watch # Watch mode
|
|
bun test --coverage # With coverage
|
|
```
|
|
|
|
**Building**:
|
|
|
|
```bash
|
|
bun build ./index.ts --outfile dist/bundle.js
|
|
bun build ./index.ts --compile --outfile myapp # Standalone executable
|
|
```
|
|
|
|
</runtime_basics>
|
|
|
|
## File Operations
|
|
|
|
<file_operations>
|
|
|
|
```typescript
|
|
// Read file (lazy, efficient)
|
|
const file = Bun.file('./data.json');
|
|
if (!(await file.exists())) throw new Error('File not found');
|
|
|
|
// Read formats
|
|
const text = await file.text();
|
|
const json = await file.json();
|
|
const buffer = await file.arrayBuffer();
|
|
const stream = file.stream(); // Large files
|
|
|
|
// Metadata
|
|
console.log(file.size, file.type);
|
|
|
|
// Write
|
|
await Bun.write('./output.txt', 'content');
|
|
await Bun.write('./data.json', JSON.stringify(data));
|
|
await Bun.write('./blob.txt', new Blob(['data']));
|
|
```
|
|
|
|
</file_operations>
|
|
|
|
## SQLite (bun:sqlite)
|
|
|
|
<sqlite>
|
|
|
|
```typescript
|
|
import { Database } from 'bun:sqlite';
|
|
|
|
const db = new Database('app.db', { create: true, readwrite: true, strict: true });
|
|
|
|
// Create tables
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
email TEXT UNIQUE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
// Prepared statements (always use these)
|
|
const getUser = db.prepare('SELECT * FROM users WHERE id = ?');
|
|
const createUser = db.prepare('INSERT INTO users (id, email, name) VALUES (?, ?, ?) RETURNING *');
|
|
|
|
// Execution
|
|
const user = getUser.get('user-123'); // Single row
|
|
const all = db.prepare('SELECT * FROM users').all(); // All rows
|
|
db.prepare('DELETE FROM users WHERE id = ?').run('id'); // No return
|
|
|
|
// Named parameters
|
|
const stmt = db.prepare('SELECT * FROM users WHERE email = $email');
|
|
stmt.get({ $email: 'alice@example.com' });
|
|
|
|
// Transactions (atomic, auto-rollback on error)
|
|
const transfer = db.transaction((fromId: string, toId: string, amount: number) => {
|
|
db.run('UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId]);
|
|
db.run('UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId]);
|
|
});
|
|
transfer('alice', 'bob', 100);
|
|
|
|
db.close(); // When done
|
|
```
|
|
|
|
See [sqlite-patterns.md](references/sqlite-patterns.md) for migrations, pooling, repository pattern.
|
|
|
|
</sqlite>
|
|
|
|
## Password Hashing
|
|
|
|
<password>
|
|
|
|
```typescript
|
|
// Hash (argon2id recommended)
|
|
const hash = await Bun.password.hash('password123', {
|
|
algorithm: 'argon2id',
|
|
memoryCost: 65536, // 64 MB
|
|
timeCost: 3
|
|
});
|
|
|
|
// Or bcrypt
|
|
const bcryptHash = await Bun.password.hash('password123', {
|
|
algorithm: 'bcrypt',
|
|
cost: 12
|
|
});
|
|
|
|
// Verify
|
|
const isValid = await Bun.password.verify('password123', hash);
|
|
if (!isValid) throw new Error('Invalid password');
|
|
```
|
|
|
|
**Auth flow example**:
|
|
|
|
```typescript
|
|
app.post('/auth/register', zValidator('json', RegisterSchema), async (c) => {
|
|
const { email, password } = c.req.valid('json');
|
|
const db = c.get('db');
|
|
|
|
if (db.prepare('SELECT id FROM users WHERE email = ?').get(email)) {
|
|
throw new HTTPException(409, { message: 'Email already registered' });
|
|
}
|
|
|
|
const hashedPassword = await Bun.password.hash(password, { algorithm: 'argon2id' });
|
|
const user = db.prepare(`
|
|
INSERT INTO users (id, email, password) VALUES (?, ?, ?) RETURNING id, email
|
|
`).get(crypto.randomUUID(), email, hashedPassword);
|
|
|
|
return c.json({ user }, 201);
|
|
});
|
|
```
|
|
|
|
</password>
|
|
|
|
## HTTP Server
|
|
|
|
<http_server>
|
|
|
|
```typescript
|
|
Bun.serve({
|
|
port: 3000,
|
|
fetch(req) {
|
|
const url = new URL(req.url);
|
|
if (url.pathname === '/') return new Response('Hello');
|
|
if (url.pathname === '/json') return Response.json({ ok: true });
|
|
return new Response('Not found', { status: 404 });
|
|
},
|
|
error(err) {
|
|
return new Response(`Error: ${err.message}`, { status: 500 });
|
|
}
|
|
});
|
|
```
|
|
|
|
**With Hono** (recommended for APIs):
|
|
|
|
```typescript
|
|
import { Hono } from 'hono';
|
|
|
|
const app = new Hono()
|
|
.get('/', (c) => c.text('Hello'))
|
|
.get('/json', (c) => c.json({ ok: true }));
|
|
|
|
Bun.serve({ port: 3000, fetch: app.fetch });
|
|
```
|
|
|
|
See [server-patterns.md](references/server-patterns.md) for routing, middleware, file serving, streaming.
|
|
|
|
</http_server>
|
|
|
|
## WebSocket
|
|
|
|
<websocket>
|
|
|
|
```typescript
|
|
import type { ServerWebSocket } from 'bun';
|
|
|
|
type WsData = { userId: string };
|
|
|
|
Bun.serve<WsData>({
|
|
port: 3000,
|
|
fetch(req, server) {
|
|
const url = new URL(req.url);
|
|
if (url.pathname === '/ws') {
|
|
const userId = url.searchParams.get('userId') || 'anon';
|
|
return server.upgrade(req, { data: { userId } }) ? undefined
|
|
: new Response('Upgrade failed', { status: 400 });
|
|
}
|
|
return new Response('Hello');
|
|
},
|
|
websocket: {
|
|
open(ws: ServerWebSocket<WsData>) {
|
|
ws.subscribe('chat');
|
|
ws.send(JSON.stringify({ type: 'connected' }));
|
|
},
|
|
message(ws: ServerWebSocket<WsData>, msg: string | Buffer) {
|
|
ws.publish('chat', msg);
|
|
},
|
|
close(ws: ServerWebSocket<WsData>) {
|
|
ws.unsubscribe('chat');
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
See [server-patterns.md](references/server-patterns.md) for client tracking, rooms, reconnection.
|
|
|
|
</websocket>
|
|
|
|
## Shell Operations
|
|
|
|
<shell>
|
|
|
|
```typescript
|
|
import { $ } from 'bun';
|
|
|
|
// Run commands
|
|
const result = await $`ls -la`;
|
|
console.log(result.text());
|
|
|
|
// Variables (auto-escaped)
|
|
const dir = './src';
|
|
await $`find ${dir} -name "*.ts"`;
|
|
|
|
// Check exit code
|
|
const { exitCode } = await $`npm test`.nothrow();
|
|
if (exitCode !== 0) console.error('Tests failed');
|
|
|
|
// Spawn process
|
|
const proc = Bun.spawn(['ls', '-la']);
|
|
await proc.exited;
|
|
|
|
// Capture output
|
|
const proc2 = Bun.spawn(['echo', 'Hello'], { stdout: 'pipe' });
|
|
const output = await new Response(proc2.stdout).text();
|
|
```
|
|
|
|
</shell>
|
|
|
|
## Testing (bun:test)
|
|
|
|
<testing>
|
|
|
|
```typescript
|
|
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
|
|
describe('feature', () => {
|
|
let db: Database;
|
|
|
|
beforeEach(() => { db = new Database(':memory:'); });
|
|
afterEach(() => { db.close(); });
|
|
|
|
test('behavior', () => {
|
|
expect(result).toBe(expected);
|
|
expect(arr).toContain(item);
|
|
expect(fn).toThrow();
|
|
expect(obj).toEqual({ foo: 'bar' });
|
|
});
|
|
|
|
test('async', async () => {
|
|
const result = await asyncFn();
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
test.todo('pending feature');
|
|
test.skip('temporarily disabled');
|
|
});
|
|
```
|
|
|
|
```bash
|
|
bun test # All tests
|
|
bun test src/api.test.ts # Specific file
|
|
bun test --watch # Watch mode
|
|
bun test --coverage # With coverage
|
|
```
|
|
|
|
See [testing.md](references/testing.md) for assertions, mocking, snapshots, best practices.
|
|
|
|
</testing>
|
|
|
|
## Environment Variables
|
|
|
|
<environment>
|
|
|
|
```typescript
|
|
// Access
|
|
console.log(Bun.env.NODE_ENV);
|
|
console.log(Bun.env.DATABASE_URL);
|
|
|
|
// Zod validation
|
|
import { z } from 'zod';
|
|
|
|
const EnvSchema = z.object({
|
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
DATABASE_URL: z.string(),
|
|
PORT: z.coerce.number().int().positive().default(3000),
|
|
API_KEY: z.string().min(32)
|
|
});
|
|
|
|
export const env = EnvSchema.parse(Bun.env);
|
|
```
|
|
|
|
Bun auto-loads `.env`, `.env.local`, `.env.production`.
|
|
|
|
</environment>
|
|
|
|
## Performance Utilities
|
|
|
|
<performance>
|
|
|
|
```typescript
|
|
// High-resolution timing
|
|
const start = Bun.nanoseconds();
|
|
await doWork();
|
|
console.log(`Took ${(Bun.nanoseconds() - start) / 1_000_000}ms`);
|
|
|
|
// Hashing
|
|
const hash = Bun.hash(data);
|
|
const crc32 = Bun.hash.crc32(data);
|
|
const sha256 = Bun.CryptoHasher.hash('sha256', data);
|
|
|
|
// Sleep
|
|
await Bun.sleep(1000);
|
|
|
|
// Memory
|
|
const { rss, heapUsed } = process.memoryUsage();
|
|
console.log('RSS:', rss / 1024 / 1024, 'MB');
|
|
```
|
|
|
|
</performance>
|
|
|
|
## Building & Bundling
|
|
|
|
<building>
|
|
|
|
```bash
|
|
# Production bundle
|
|
bun build ./index.ts --outfile dist/bundle.js --minify --sourcemap
|
|
|
|
# External deps
|
|
bun build ./index.ts --outfile dist/bundle.js --external hono --external zod
|
|
|
|
# Standalone executable
|
|
bun build ./index.ts --compile --outfile myapp
|
|
|
|
# Cross-compile
|
|
bun build ./index.ts --compile --target=bun-linux-x64 --outfile myapp-linux
|
|
bun build ./index.ts --compile --target=bun-darwin-arm64 --outfile myapp-macos
|
|
bun build ./index.ts --compile --target=bun-windows-x64 --outfile myapp.exe
|
|
```
|
|
|
|
</building>
|
|
|
|
<rules>
|
|
|
|
ALWAYS:
|
|
- Use Bun APIs when available (faster, native)
|
|
- Prepared statements for database queries
|
|
- Transactions for multi-statement operations
|
|
- argon2id for password hashing
|
|
- Validate environment variables at startup
|
|
- Close database connections when done
|
|
|
|
NEVER:
|
|
- String interpolation in SQL (use parameters)
|
|
- Plaintext passwords
|
|
- Ignore async disposal cleanup
|
|
- Deprecated Node.js APIs when Bun native exists
|
|
|
|
PREFER:
|
|
- Bun.file over fs.readFile
|
|
- Bun.write over fs.writeFile
|
|
- bun:sqlite over external SQLite libraries
|
|
- Bun.password over bcrypt/argon2 packages
|
|
- $ shell template over child_process
|
|
|
|
</rules>
|
|
|
|
<references>
|
|
|
|
- [sqlite-patterns.md](references/sqlite-patterns.md) — migrations, pooling, repository, FTS
|
|
- [server-patterns.md](references/server-patterns.md) — HTTP, WebSocket, streaming, compression
|
|
- [testing.md](references/testing.md) — assertions, mocking, snapshots, best practices
|
|
|
|
**Examples:**
|
|
- [database-crud.md](examples/database-crud.md) — SQLite CRUD patterns
|
|
- [file-uploads.md](examples/file-uploads.md) — streaming file handling
|
|
|
|
</references>
|