initial commit
This commit is contained in:
440
.github/copilot-cli-instructions.md
vendored
Normal file
440
.github/copilot-cli-instructions.md
vendored
Normal file
@@ -0,0 +1,440 @@
|
||||
# GitHub Copilot CLI Instructions for bDS
|
||||
|
||||
Quick reference for using GitHub Copilot CLI (`gh copilot`) with this Electron + TypeScript + SQLite project.
|
||||
|
||||
## Project Context for CLI Queries
|
||||
|
||||
When asking Copilot CLI for help, provide this context for better responses:
|
||||
|
||||
```
|
||||
This is an Electron app using TypeScript, React, Drizzle ORM, and @libsql/client for SQLite.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common CLI Patterns
|
||||
|
||||
### Build & Development
|
||||
|
||||
```bash
|
||||
# Ask how to run in development
|
||||
gh copilot suggest "how to run electron app in dev mode with vite HMR"
|
||||
|
||||
# Ask about TypeScript compilation
|
||||
gh copilot suggest "compile typescript for electron main process with tsconfig"
|
||||
|
||||
# Build commands for this project
|
||||
npm run build:main # TypeScript -> JavaScript for main process
|
||||
npm run build:renderer # Vite build for React renderer
|
||||
npm run build # Both
|
||||
npm start # Launch Electron
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
|
||||
```bash
|
||||
# Generate Drizzle migrations
|
||||
gh copilot suggest "generate drizzle orm migration for sqlite"
|
||||
|
||||
# Query patterns
|
||||
gh copilot suggest "drizzle orm query to select with where clause typescript"
|
||||
|
||||
# Turso sync
|
||||
gh copilot suggest "sync local sqlite with turso remote database libsql"
|
||||
```
|
||||
|
||||
### Electron Commands
|
||||
|
||||
```bash
|
||||
# IPC patterns
|
||||
gh copilot suggest "electron ipc invoke handler with typescript types"
|
||||
|
||||
# Window management
|
||||
gh copilot suggest "electron create browser window with preload script"
|
||||
|
||||
# Menu creation
|
||||
gh copilot suggest "electron application menu with keyboard shortcuts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Quick Fixes
|
||||
|
||||
### Type Errors
|
||||
|
||||
```bash
|
||||
# When you see "Type X is not assignable to type Y"
|
||||
gh copilot explain "typescript error Type X is not assignable to type Y"
|
||||
|
||||
# For generic type issues
|
||||
gh copilot suggest "typescript generic function that returns same type as input"
|
||||
|
||||
# Promise/async issues
|
||||
gh copilot suggest "typescript async function return type Promise"
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
```bash
|
||||
# Discriminated unions
|
||||
gh copilot suggest "typescript discriminated union for status types"
|
||||
|
||||
# Type guards
|
||||
gh copilot suggest "typescript type guard function to narrow union type"
|
||||
|
||||
# Utility types
|
||||
gh copilot suggest "typescript Pick Omit Partial for interface modification"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SQLite & Drizzle Queries
|
||||
|
||||
### Schema Definition
|
||||
|
||||
```bash
|
||||
# Create new table
|
||||
gh copilot suggest "drizzle orm sqlite table schema with text integer columns"
|
||||
|
||||
# Add index
|
||||
gh copilot suggest "drizzle orm create index on sqlite table"
|
||||
|
||||
# Relations
|
||||
gh copilot suggest "drizzle orm one to many relation sqlite"
|
||||
```
|
||||
|
||||
### Query Building
|
||||
|
||||
```bash
|
||||
# Select with conditions
|
||||
gh copilot suggest "drizzle orm select where equals and order by"
|
||||
|
||||
# Insert with returning
|
||||
gh copilot suggest "drizzle orm insert returning inserted row"
|
||||
|
||||
# Update with conditions
|
||||
gh copilot suggest "drizzle orm update set where condition"
|
||||
|
||||
# Transaction
|
||||
gh copilot suggest "drizzle orm transaction with multiple operations"
|
||||
```
|
||||
|
||||
### LibSQL Specifics
|
||||
|
||||
```bash
|
||||
# Local file database
|
||||
gh copilot suggest "libsql client connect to local sqlite file"
|
||||
|
||||
# Remote Turso connection
|
||||
gh copilot suggest "libsql client connect turso with auth token"
|
||||
|
||||
# Execute multiple statements
|
||||
gh copilot suggest "libsql execute multiple sql statements batch"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sync Implementation
|
||||
|
||||
### Conflict Detection
|
||||
|
||||
```bash
|
||||
gh copilot suggest "detect sync conflicts using checksums typescript"
|
||||
gh copilot suggest "last-write-wins conflict resolution pattern"
|
||||
gh copilot suggest "vector clock for distributed sync"
|
||||
```
|
||||
|
||||
### Offline Queue
|
||||
|
||||
```bash
|
||||
gh copilot suggest "queue offline operations for later sync typescript"
|
||||
gh copilot suggest "exponential backoff retry logic async"
|
||||
gh copilot suggest "detect online offline status in electron"
|
||||
```
|
||||
|
||||
### Sync Status
|
||||
|
||||
```bash
|
||||
gh copilot suggest "track sync status pending synced error per record"
|
||||
gh copilot suggest "sync log table for audit trail"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React & Zustand
|
||||
|
||||
### Store Patterns
|
||||
|
||||
```bash
|
||||
gh copilot suggest "zustand store with typescript typed actions"
|
||||
gh copilot suggest "zustand selector to prevent unnecessary rerenders"
|
||||
gh copilot suggest "zustand persist middleware for local storage"
|
||||
```
|
||||
|
||||
### Component Patterns
|
||||
|
||||
```bash
|
||||
gh copilot suggest "react component with zustand store hook"
|
||||
gh copilot suggest "react useEffect cleanup for ipc listener"
|
||||
gh copilot suggest "react context provider with typescript"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Electron Security
|
||||
|
||||
```bash
|
||||
# Secure IPC
|
||||
gh copilot suggest "electron context bridge expose api safely"
|
||||
|
||||
# Input validation
|
||||
gh copilot suggest "validate ipc handler input zod typescript"
|
||||
|
||||
# Content Security Policy
|
||||
gh copilot suggest "electron content security policy meta tag"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Operations
|
||||
|
||||
### Markdown with Frontmatter
|
||||
|
||||
```bash
|
||||
gh copilot suggest "read markdown file with yaml frontmatter gray-matter"
|
||||
gh copilot suggest "write markdown file with yaml frontmatter node"
|
||||
gh copilot suggest "parse yaml frontmatter to typescript type"
|
||||
```
|
||||
|
||||
### Media Files
|
||||
|
||||
```bash
|
||||
gh copilot suggest "copy file to destination directory nodejs"
|
||||
gh copilot suggest "get image dimensions from file nodejs"
|
||||
gh copilot suggest "calculate file checksum md5 nodejs"
|
||||
```
|
||||
|
||||
### Sidecar Pattern
|
||||
|
||||
```bash
|
||||
gh copilot suggest "json sidecar metadata file for binary asset"
|
||||
gh copilot suggest "read write json file atomic operation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
```bash
|
||||
# Custom errors
|
||||
gh copilot suggest "typescript custom error class with error code"
|
||||
|
||||
# Result types
|
||||
gh copilot suggest "typescript result type success or error pattern"
|
||||
|
||||
# Async error handling
|
||||
gh copilot suggest "try catch async await with typed error"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test-Driven Development (TDD)
|
||||
|
||||
**This project requires TDD. Write tests BEFORE implementation.**
|
||||
|
||||
### TDD Workflow
|
||||
|
||||
```bash
|
||||
# Step 1: Write failing test first
|
||||
gh copilot suggest "vitest test for function that does X should return Y"
|
||||
|
||||
# Step 2: Make test pass with minimal code
|
||||
gh copilot suggest "minimal implementation to make vitest test pass"
|
||||
|
||||
# Step 3: Refactor while keeping tests green
|
||||
gh copilot suggest "refactor function for readability typescript"
|
||||
```
|
||||
|
||||
### Test Commands
|
||||
|
||||
```bash
|
||||
npm run test # Run all tests once
|
||||
npm run test:watch # Watch mode (re-run on changes)
|
||||
npm run test:coverage # Generate coverage report
|
||||
npm run test:ui # Open Vitest UI in browser
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
```bash
|
||||
# Create unit test
|
||||
gh copilot suggest "vitest unit test for typescript async function"
|
||||
|
||||
# Test with mocks
|
||||
gh copilot suggest "vitest mock module fs promises typescript"
|
||||
|
||||
# Test event emitters
|
||||
gh copilot suggest "vitest test eventemitter emit and listen"
|
||||
|
||||
# Test error cases
|
||||
gh copilot suggest "vitest test async function throws error"
|
||||
```
|
||||
|
||||
### Mock Patterns
|
||||
|
||||
```bash
|
||||
# Mock database
|
||||
gh copilot suggest "vitest mock drizzle orm database connection"
|
||||
|
||||
# Mock file system
|
||||
gh copilot suggest "vitest mock fs promises readFile writeFile"
|
||||
|
||||
# Mock Electron
|
||||
gh copilot suggest "vitest mock electron app ipcMain"
|
||||
|
||||
# Factory functions
|
||||
gh copilot suggest "typescript factory function create test data with overrides"
|
||||
```
|
||||
|
||||
### Coverage
|
||||
|
||||
```bash
|
||||
# Check coverage
|
||||
gh copilot suggest "vitest coverage v8 configuration"
|
||||
|
||||
# Coverage thresholds
|
||||
gh copilot suggest "vitest minimum coverage threshold configuration"
|
||||
```
|
||||
|
||||
### Testing Specific Scenarios
|
||||
|
||||
```bash
|
||||
# Task management tests
|
||||
gh copilot suggest "vitest test async task queue with progress callback"
|
||||
|
||||
# Sync engine tests
|
||||
gh copilot suggest "vitest test sync conflict detection by checksum"
|
||||
|
||||
# Post engine tests
|
||||
gh copilot suggest "vitest test markdown frontmatter parse and serialize"
|
||||
|
||||
# Media tests
|
||||
gh copilot suggest "vitest test file checksum calculation md5"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git Operations
|
||||
|
||||
```bash
|
||||
# Commits
|
||||
gh copilot suggest "git commit message for feature typescript"
|
||||
|
||||
# Branching
|
||||
gh copilot suggest "git branch naming convention feature bugfix"
|
||||
|
||||
# Stashing
|
||||
gh copilot suggest "git stash changes and apply later"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project-Specific Commands
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| Install deps | `npm install` |
|
||||
| Dev mode | `npm run dev` (runs Vite + TSC watch) |
|
||||
| Build all | `npm run build` |
|
||||
| Start app | `npm start` |
|
||||
| Run tests | `npm run test` |
|
||||
| Watch tests | `npm run test:watch` |
|
||||
| Coverage | `npm run test:coverage` |
|
||||
| Build main only | `npm run build:main` |
|
||||
| Build renderer only | `npm run build:renderer` |
|
||||
|
||||
### File Locations
|
||||
|
||||
| What | Where |
|
||||
|------|-------|
|
||||
| Electron main | `src/main/` |
|
||||
| React renderer | `src/renderer/` |
|
||||
| Database schema | `src/main/database/schema.ts` |
|
||||
| Engine classes | `src/main/engine/` |
|
||||
| IPC handlers | `src/main/ipc/handlers.ts` |
|
||||
| React components | `src/renderer/components/` |
|
||||
| State store | `src/renderer/store/appStore.ts` |
|
||||
|
||||
### Adding a Feature Checklist
|
||||
|
||||
```bash
|
||||
# 1. Schema (if new data)
|
||||
gh copilot suggest "add column to drizzle sqlite table"
|
||||
|
||||
# 2. Engine method
|
||||
gh copilot suggest "engine class method with event emitter typescript"
|
||||
|
||||
# 3. IPC handler
|
||||
gh copilot suggest "electron ipc handle invoke pattern"
|
||||
|
||||
# 4. Preload exposure
|
||||
gh copilot suggest "electron preload contextBridge expose"
|
||||
|
||||
# 5. Store action
|
||||
gh copilot suggest "zustand action to update state"
|
||||
|
||||
# 6. UI component
|
||||
gh copilot suggest "react component call electron api"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
```bash
|
||||
# Module not found
|
||||
gh copilot explain "cannot find module typescript error electron"
|
||||
|
||||
# IPC not working
|
||||
gh copilot explain "electron ipc invoke not receiving response"
|
||||
|
||||
# Database locked
|
||||
gh copilot explain "sqlite database is locked error"
|
||||
|
||||
# Build fails
|
||||
gh copilot explain "vite build error cannot resolve module"
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
```bash
|
||||
# Enable source maps
|
||||
gh copilot suggest "typescript source maps for electron debugging"
|
||||
|
||||
# DevTools
|
||||
gh copilot suggest "open chrome devtools in electron app"
|
||||
|
||||
# Logging
|
||||
gh copilot suggest "electron log to file in main process"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
When asking Copilot CLI for code in this project:
|
||||
|
||||
1. **Specify TypeScript** - Always mention "typescript" for typed responses
|
||||
2. **Mention Electron context** - "main process" or "renderer process"
|
||||
3. **Reference Drizzle** - Use "drizzle orm" not "raw sql"
|
||||
4. **Async patterns** - Request "async/await" not callbacks
|
||||
5. **Type safety** - Ask for "typed" or "with types" versions
|
||||
|
||||
Example well-formed query:
|
||||
```bash
|
||||
gh copilot suggest "typescript async function to update drizzle orm record with transaction in electron main process"
|
||||
```
|
||||
665
.github/copilot-instructions.md
vendored
Normal file
665
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,665 @@
|
||||
# GitHub Copilot Instructions for Blogging Desktop Server (bDS)
|
||||
|
||||
This document provides context and best practices for GitHub Copilot when working on this Electron + TypeScript + SQLite blogging application.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Blogging Desktop Server (bDS)** is a desktop blogging application built with:
|
||||
- **Electron** v28+ for cross-platform desktop
|
||||
- **TypeScript** for all code (strict mode)
|
||||
- **React** for the renderer UI
|
||||
- **Drizzle ORM** for type-safe database access
|
||||
- **@libsql/client** for SQLite (local) and Turso (cloud sync)
|
||||
- **Zustand** for React state management
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
### Separation of Concerns
|
||||
|
||||
1. **Engine Classes** (`src/main/engine/`): All business logic lives here
|
||||
- `PostEngine`: Blog post CRUD, file I/O, markdown handling
|
||||
- `MediaEngine`: Media import, metadata management
|
||||
- `SyncEngine`: Remote database synchronization
|
||||
- `TaskManager`: Async task queue with progress tracking
|
||||
|
||||
2. **IPC Layer** (`src/main/ipc/`): Bridge between main and renderer
|
||||
- Handlers call engine methods, never contain business logic
|
||||
- Always use `ipcMain.handle` for async operations
|
||||
|
||||
3. **UI Components** (`src/renderer/components/`): Presentation only
|
||||
- Components should be stateless where possible
|
||||
- Use Zustand store for shared state
|
||||
- Never call IPC directly from deeply nested components
|
||||
|
||||
### File Storage Pattern
|
||||
|
||||
- **Posts**: Markdown files with YAML frontmatter in `posts/` directory
|
||||
- **Media**: Binary files with `.meta` JSON sidecar files in `media/` directory
|
||||
- **Database**: Index/cache only, source of truth is filesystem
|
||||
|
||||
## TypeScript Best Practices
|
||||
|
||||
### Type Safety
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Use explicit return types for public methods
|
||||
async function getPost(id: string): Promise<PostData | null> {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ DO: Use discriminated unions for status types
|
||||
type SyncStatus = 'pending' | 'syncing' | 'synced' | 'error';
|
||||
|
||||
// ✅ DO: Use Drizzle's inferred types
|
||||
import { posts, type Post, type NewPost } from './schema';
|
||||
|
||||
// ❌ DON'T: Use `any` type
|
||||
function processData(data: any) { } // Bad
|
||||
|
||||
// ❌ DON'T: Ignore null/undefined possibilities
|
||||
const post = await getPost(id);
|
||||
console.log(post.title); // Bad - post might be null
|
||||
```
|
||||
|
||||
### Async/Await Patterns
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Use try-catch with specific error handling
|
||||
try {
|
||||
await fileOperation();
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('ENOENT')) {
|
||||
// Handle file not found
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// ✅ DO: Use Promise.all for concurrent operations
|
||||
const [posts, media] = await Promise.all([
|
||||
postEngine.getAllPosts(),
|
||||
mediaEngine.getAllMedia()
|
||||
]);
|
||||
|
||||
// ❌ DON'T: Forget to await async operations
|
||||
savePost(post); // Bad - fire and forget
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Create typed error classes
|
||||
class SyncError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code: 'AUTH_FAILED' | 'NETWORK_ERROR' | 'CONFLICT'
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'SyncError';
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ DO: Return result objects for operations that can fail
|
||||
interface OperationResult<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Electron Best Practices
|
||||
|
||||
### IPC Communication
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Use contextBridge for secure IPC exposure
|
||||
// preload.ts
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
posts: {
|
||||
getAll: () => ipcRenderer.invoke('posts:getAll'),
|
||||
create: (data: CreatePostInput) => ipcRenderer.invoke('posts:create', data),
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ DO: Validate all IPC inputs in main process
|
||||
ipcMain.handle('posts:create', async (_event, data: unknown) => {
|
||||
const validated = validateCreatePostInput(data);
|
||||
return postEngine.createPost(validated);
|
||||
});
|
||||
|
||||
// ❌ DON'T: Use nodeIntegration: true
|
||||
// ❌ DON'T: Expose Node.js APIs directly to renderer
|
||||
```
|
||||
|
||||
### Window Management
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Check window existence before sending messages
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('event', data);
|
||||
}
|
||||
|
||||
// ✅ DO: Clean up IPC handlers on window close
|
||||
mainWindow.on('closed', () => {
|
||||
ipcMain.removeHandler('posts:getAll');
|
||||
});
|
||||
```
|
||||
|
||||
### Process Separation
|
||||
|
||||
```typescript
|
||||
// Main process: File system, database, native APIs
|
||||
// Renderer process: UI only, no direct fs/db access
|
||||
|
||||
// ✅ DO: All file operations in main process
|
||||
// src/main/engine/PostEngine.ts
|
||||
await fs.writeFile(filePath, content);
|
||||
|
||||
// ❌ DON'T: Import 'fs' in renderer
|
||||
// src/renderer/components/Editor.tsx
|
||||
import fs from 'fs'; // Bad!
|
||||
```
|
||||
|
||||
## SQLite & Drizzle ORM Best Practices
|
||||
|
||||
### Schema Definition
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Use proper column types and constraints
|
||||
export const posts = sqliteTable('posts', {
|
||||
id: text('id').primaryKey(),
|
||||
title: text('title').notNull(),
|
||||
slug: text('slug').notNull().unique(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
syncStatus: text('sync_status').$type<'pending' | 'synced'>().default('pending'),
|
||||
});
|
||||
|
||||
// ✅ DO: Create indexes for frequently queried columns
|
||||
// CREATE INDEX idx_posts_slug ON posts(slug);
|
||||
// CREATE INDEX idx_posts_sync_status ON posts(sync_status);
|
||||
```
|
||||
|
||||
### Query Patterns
|
||||
|
||||
```typescript
|
||||
import { eq, and, desc, sql } from 'drizzle-orm';
|
||||
|
||||
// ✅ DO: Use Drizzle query builder
|
||||
const drafts = await db
|
||||
.select()
|
||||
.from(posts)
|
||||
.where(eq(posts.status, 'draft'))
|
||||
.orderBy(desc(posts.updatedAt));
|
||||
|
||||
// ✅ DO: Use transactions for multi-table updates
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.update(posts).set({ status: 'published' }).where(eq(posts.id, id));
|
||||
await tx.insert(syncLog).values({ entityId: id, operation: 'update' });
|
||||
});
|
||||
|
||||
// ❌ DON'T: Write raw SQL unless absolutely necessary
|
||||
// ❌ DON'T: Use string interpolation in queries
|
||||
const result = db.run(`SELECT * FROM posts WHERE id = '${id}'`); // SQL injection risk!
|
||||
```
|
||||
|
||||
### Connection Management
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Use singleton pattern for database connection
|
||||
let dbInstance: DatabaseConnection | null = null;
|
||||
|
||||
export function getDatabase(): DatabaseConnection {
|
||||
if (!dbInstance) {
|
||||
dbInstance = new DatabaseConnection();
|
||||
}
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
// ✅ DO: Close connection gracefully on app quit
|
||||
app.on('before-quit', async () => {
|
||||
await getDatabase().close();
|
||||
});
|
||||
```
|
||||
|
||||
## Remote Sync Best Practices (Turso/LibSQL)
|
||||
|
||||
### Sync Strategy
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Track sync status per entity
|
||||
interface SyncableEntity {
|
||||
syncStatus: 'pending' | 'syncing' | 'synced' | 'conflict';
|
||||
syncedAt: number | null;
|
||||
checksum: string;
|
||||
}
|
||||
|
||||
// ✅ DO: Use checksums to detect conflicts
|
||||
const localChecksum = calculateChecksum(localContent);
|
||||
const remoteChecksum = await fetchRemoteChecksum(id);
|
||||
if (localChecksum !== remoteChecksum) {
|
||||
// Handle conflict
|
||||
}
|
||||
|
||||
// ✅ DO: Implement retry logic with exponential backoff
|
||||
async function syncWithRetry(maxAttempts = 3): Promise<void> {
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
await sync();
|
||||
return;
|
||||
} catch (error) {
|
||||
if (attempt === maxAttempts) throw error;
|
||||
await sleep(Math.pow(2, attempt) * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Offline-First Approach
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Always write to local first
|
||||
async function savePost(post: PostData): Promise<void> {
|
||||
// 1. Save to local file system
|
||||
await writePostFile(post);
|
||||
|
||||
// 2. Update local database
|
||||
await updateLocalDatabase(post);
|
||||
|
||||
// 3. Mark for sync (don't wait for network)
|
||||
await markForSync(post.id);
|
||||
}
|
||||
|
||||
// ✅ DO: Queue sync operations
|
||||
const syncQueue: SyncOperation[] = [];
|
||||
|
||||
function queueSync(operation: SyncOperation): void {
|
||||
syncQueue.push(operation);
|
||||
processSyncQueue(); // Non-blocking
|
||||
}
|
||||
|
||||
// ✅ DO: Handle network state changes
|
||||
window.addEventListener('online', () => {
|
||||
syncEngine.processPendingChanges();
|
||||
});
|
||||
```
|
||||
|
||||
### Conflict Resolution
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Implement clear conflict resolution strategy
|
||||
type ConflictResolution = 'local-wins' | 'remote-wins' | 'manual';
|
||||
|
||||
interface ConflictInfo {
|
||||
entityId: string;
|
||||
localVersion: EntityVersion;
|
||||
remoteVersion: EntityVersion;
|
||||
resolution?: ConflictResolution;
|
||||
}
|
||||
|
||||
// ✅ DO: Allow user to resolve conflicts manually when needed
|
||||
async function resolveConflict(
|
||||
conflict: ConflictInfo,
|
||||
resolution: ConflictResolution
|
||||
): Promise<void> {
|
||||
switch (resolution) {
|
||||
case 'local-wins':
|
||||
await pushLocalToRemote(conflict.entityId);
|
||||
break;
|
||||
case 'remote-wins':
|
||||
await pullRemoteToLocal(conflict.entityId);
|
||||
break;
|
||||
case 'manual':
|
||||
await showConflictResolutionUI(conflict);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sync Logging
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Log all sync operations for debugging
|
||||
await db.insert(syncLog).values({
|
||||
id: generateId(),
|
||||
entityType: 'post',
|
||||
entityId: postId,
|
||||
operation: 'push',
|
||||
status: 'success',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// ✅ DO: Implement sync status visibility in UI
|
||||
const pendingCount = await syncEngine.getPendingChangesCount();
|
||||
statusBar.setSyncStatus({ pending: pendingCount });
|
||||
```
|
||||
|
||||
## React & State Management
|
||||
|
||||
### Zustand Patterns
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Keep store slices focused
|
||||
interface AppState {
|
||||
// UI state
|
||||
activeView: 'posts' | 'media' | 'settings';
|
||||
sidebarVisible: boolean;
|
||||
|
||||
// Data
|
||||
posts: PostData[];
|
||||
media: MediaData[];
|
||||
|
||||
// Actions
|
||||
setActiveView: (view: AppState['activeView']) => void;
|
||||
setPosts: (posts: PostData[]) => void;
|
||||
}
|
||||
|
||||
// ✅ DO: Use selectors for derived state
|
||||
const draftCount = useAppStore((state) =>
|
||||
state.posts.filter(p => p.status === 'draft').length
|
||||
);
|
||||
|
||||
// ❌ DON'T: Store derived data
|
||||
interface BadState {
|
||||
posts: PostData[];
|
||||
draftCount: number; // Bad - derived from posts
|
||||
}
|
||||
```
|
||||
|
||||
### Component Patterns
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Use composition over prop drilling
|
||||
<EditorProvider postId={selectedPostId}>
|
||||
<EditorToolbar />
|
||||
<EditorContent />
|
||||
<EditorStatusBar />
|
||||
</EditorProvider>
|
||||
|
||||
// ✅ DO: Separate container and presentation components
|
||||
// Container: handles data fetching/IPC
|
||||
// Presentation: pure rendering based on props
|
||||
|
||||
// ❌ DON'T: Put IPC calls in deeply nested components
|
||||
const DeepComponent = () => {
|
||||
// Bad - hard to test and maintain
|
||||
const handleClick = async () => {
|
||||
await window.electronAPI.posts.save(data);
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
```
|
||||
src/
|
||||
main/
|
||||
database/
|
||||
schema.ts # Drizzle schema definitions
|
||||
connection.ts # Database connection management
|
||||
engine/
|
||||
PostEngine.ts # PascalCase for classes
|
||||
TaskManager.ts
|
||||
ipc/
|
||||
handlers.ts # camelCase for modules
|
||||
renderer/
|
||||
components/
|
||||
Editor/
|
||||
Editor.tsx # PascalCase for components
|
||||
Editor.css # Matching CSS file
|
||||
index.ts # Barrel exports
|
||||
store/
|
||||
appStore.ts # camelCase for stores
|
||||
types/
|
||||
electron.d.ts # Type declarations
|
||||
```
|
||||
|
||||
## Test-Driven Development (TDD) Requirements
|
||||
|
||||
**This project follows strict TDD practices. All new features MUST have tests written BEFORE implementation.**
|
||||
|
||||
### TDD Workflow (Red-Green-Refactor)
|
||||
|
||||
```typescript
|
||||
// 1. RED: Write a failing test first
|
||||
describe('PostEngine.createPost', () => {
|
||||
it('should create a post with generated slug from title', async () => {
|
||||
const result = await postEngine.createPost({ title: 'Hello World' });
|
||||
expect(result.slug).toBe('hello-world');
|
||||
});
|
||||
});
|
||||
|
||||
// 2. GREEN: Write minimal code to pass the test
|
||||
// 3. REFACTOR: Improve the code while keeping tests green
|
||||
```
|
||||
|
||||
### Test File Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── setup.ts # Global test configuration
|
||||
├── utils/
|
||||
│ ├── factories.ts # Mock data factories
|
||||
│ └── index.ts # Utility exports
|
||||
└── engine/
|
||||
├── TaskManager.test.ts # TaskManager unit tests
|
||||
├── PostEngine.test.ts # PostEngine unit tests
|
||||
├── MediaEngine.test.ts # MediaEngine unit tests
|
||||
└── SyncEngine.test.ts # SyncEngine unit tests
|
||||
```
|
||||
|
||||
### Test Naming Conventions
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Use descriptive test names that explain behavior
|
||||
it('should generate slug from title with lowercase and hyphens', () => {});
|
||||
it('should emit taskCompleted event when task finishes', () => {});
|
||||
it('should return null when post not found', () => {});
|
||||
|
||||
// ❌ DON'T: Use vague test names
|
||||
it('works', () => {});
|
||||
it('test createPost', () => {});
|
||||
```
|
||||
|
||||
### Mock Factory Patterns
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Use factory functions with overrides
|
||||
import { createMockPost, createMockMedia } from '../utils/factories';
|
||||
|
||||
const post = createMockPost({
|
||||
title: 'Custom Title',
|
||||
status: 'published'
|
||||
});
|
||||
|
||||
// ✅ DO: Create specialized factories for common scenarios
|
||||
const draftPost = createMockPost({ status: 'draft' });
|
||||
const publishedPost = createMockPublishedPost();
|
||||
const imageMedia = createMockMedia({ mimeType: 'image/jpeg' });
|
||||
const pdfMedia = createMockPdfMedia();
|
||||
```
|
||||
|
||||
### Testing Async Code
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Use async/await in tests
|
||||
it('should save post to filesystem', async () => {
|
||||
const post = createMockPost();
|
||||
await postEngine.createPost(post);
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ✅ DO: Test event emissions
|
||||
it('should emit postCreated event', async () => {
|
||||
const handler = vi.fn();
|
||||
postEngine.on('postCreated', handler);
|
||||
|
||||
await postEngine.createPost({ title: 'Test' });
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ title: 'Test' })
|
||||
);
|
||||
});
|
||||
|
||||
// ✅ DO: Test error cases
|
||||
it('should throw when file write fails', async () => {
|
||||
vi.mocked(fs.writeFile).mockRejectedValueOnce(new Error('ENOSPC'));
|
||||
|
||||
await expect(postEngine.createPost({ title: 'Test' }))
|
||||
.rejects.toThrow('ENOSPC');
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking Dependencies
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Mock at module level for consistent behavior
|
||||
vi.mock('../../src/main/database', () => ({
|
||||
getDatabase: vi.fn(() => mockDatabase),
|
||||
}));
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
readFile: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
}));
|
||||
|
||||
// ✅ DO: Reset mocks between tests
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
```
|
||||
|
||||
### Test Coverage Requirements
|
||||
|
||||
- **Minimum coverage**: 80% for engine classes
|
||||
- **Critical paths**: 100% coverage for data mutations
|
||||
- **Edge cases**: Null handling, empty arrays, error conditions
|
||||
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
npm run test:coverage
|
||||
|
||||
# View coverage report
|
||||
open coverage/index.html
|
||||
```
|
||||
|
||||
### When to Write Tests
|
||||
|
||||
| Scenario | Test Requirement |
|
||||
|----------|-----------------|
|
||||
| New engine method | Unit tests BEFORE implementation |
|
||||
| Bug fix | Failing test that reproduces bug FIRST |
|
||||
| Refactoring | Ensure existing tests pass BEFORE and AFTER |
|
||||
| IPC handler | Integration test with mocked engine |
|
||||
| UI component | Behavior tests for user interactions |
|
||||
|
||||
### Test Commands
|
||||
|
||||
```bash
|
||||
npm run test # Run all tests once
|
||||
npm run test:watch # Run tests in watch mode
|
||||
npm run test:coverage # Generate coverage report
|
||||
npm run test:ui # Open Vitest UI
|
||||
```
|
||||
|
||||
### Vitest Configuration
|
||||
|
||||
Tests are configured in `vitest.config.ts` with:
|
||||
- Global test utilities (describe, it, expect, vi)
|
||||
- Node environment for main process tests
|
||||
- Coverage reports via v8
|
||||
- Custom setup file for Electron mocks
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Design for testability
|
||||
// - Engine classes should accept dependencies via constructor
|
||||
// - Use interfaces for external dependencies
|
||||
// - Keep pure business logic separate from I/O
|
||||
|
||||
class PostEngine {
|
||||
constructor(
|
||||
private db: DatabaseConnection,
|
||||
private fs: FileSystemAdapter, // Mockable
|
||||
private eventEmitter: EventEmitter
|
||||
) {}
|
||||
}
|
||||
|
||||
// ✅ DO: Use factory functions for test data
|
||||
function createTestPost(overrides?: Partial<PostData>): PostData {
|
||||
return {
|
||||
id: 'test-id',
|
||||
title: 'Test Post',
|
||||
status: 'draft',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ DO: Test pure functions in isolation
|
||||
describe('generateSlug', () => {
|
||||
it('should convert to lowercase', () => {
|
||||
expect(generateSlug('Hello World')).toBe('hello-world');
|
||||
});
|
||||
|
||||
it('should replace special characters', () => {
|
||||
expect(generateSlug('Hello, World!')).toBe('hello-world');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Common Patterns in This Codebase
|
||||
|
||||
### Creating a New Engine Method
|
||||
|
||||
```typescript
|
||||
// 1. Add method to engine class
|
||||
async newOperation(input: InputType): Promise<OutputType> {
|
||||
// Validate input
|
||||
// Perform operation
|
||||
// Emit event for UI updates
|
||||
this.emit('operationCompleted', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 2. Add IPC handler
|
||||
ipcMain.handle('entity:operation', async (_event, input) => {
|
||||
return engine.newOperation(input);
|
||||
});
|
||||
|
||||
// 3. Expose in preload
|
||||
entity: {
|
||||
operation: (input) => ipcRenderer.invoke('entity:operation', input),
|
||||
}
|
||||
|
||||
// 4. Update store and UI
|
||||
```
|
||||
|
||||
### Adding a New Database Table
|
||||
|
||||
```typescript
|
||||
// 1. Define in schema.ts with proper types
|
||||
export const newTable = sqliteTable('new_table', {
|
||||
id: text('id').primaryKey(),
|
||||
// ... columns
|
||||
});
|
||||
|
||||
// 2. Export types
|
||||
export type NewTableRow = typeof newTable.$inferSelect;
|
||||
export type NewTableInsert = typeof newTable.$inferInsert;
|
||||
|
||||
// 3. Add CREATE TABLE to migrations in connection.ts
|
||||
// 4. Create corresponding engine class
|
||||
```
|
||||
|
||||
## Security Reminders
|
||||
|
||||
- Never log sensitive data (auth tokens, passwords)
|
||||
- Validate all IPC inputs before processing
|
||||
- Use `contextIsolation: true` and `sandbox: false` only when necessary
|
||||
- Store Turso auth tokens in secure storage, not in code
|
||||
- Sanitize user input before rendering (XSS prevention)
|
||||
Reference in New Issue
Block a user