Files
insta-recipe/docs/CODE_STYLE.md
Giancarmine Salucci 49bccf8f15 simplify
2026-02-18 01:21:44 +01:00

833 lines
16 KiB
Markdown

# Code Style Guide
**Last Updated:** 2026-02-15T00:00:00.000Z
**JIRA:** RECIPE-0001
---
## Language & Version
**Primary Language:** TypeScript
**Version:** 5.9.3
**Framework:** SvelteKit 2.48.5 with Svelte 5.43.8
**Node.js:** 22+ (LTS)
---
## Naming Conventions
### Files & Directories
#### SvelteKit Route Files
- Route pages: `+page.svelte`
- Route servers: `+server.ts`
- Route layouts: `+layout.svelte`
- Type definitions: `$types.ts` (auto-generated)
**Example:**
```
src/routes/api/queue/
├── [id]/
│ ├── +server.ts
│ └── retry/
│ └── +server.ts
├── stream/
│ └── +server.ts
└── +server.ts
```
#### Library Files
- **PascalCase** for classes and managers: `QueueManager.ts`, `PushNotificationService.ts`
- **kebab-case** for utilities and configs: `tandoor-config.ts`, `instagram-url.ts`
- **lowercase** for general modules: `browser.ts`, `extraction.ts`, `parser.ts`
**Examples from codebase:**
- `src/lib/server/queue/QueueManager.ts`
- `src/lib/server/tandoor-config.ts`
- `src/lib/client/PushNotificationManager.ts`
#### Test Files
Pattern: `<name>.spec.ts` or `<name>.test.ts`
**Examples:**
- `queue-manager.spec.ts`
- `instagram-url-validation.spec.ts`
- `page.svelte.spec.ts`
### Variables & Functions
#### Variables
- **camelCase** for local variables and parameters
- **SCREAMING_SNAKE_CASE** for constants
**Examples:**
```typescript
// From QueueManager.ts
private items: Map<string, QueueItem> = new Map();
private subscribers: Set<QueueUpdateCallback> = new Set();
// From parser.ts
const RECIPE_DETECTION_PROMPT = "...";
const RECIPE_EXTRACTION_PROMPT = "...";
// Local variables
const now = new Date().toISOString();
const unsubscribe = queueManager.subscribe(callback);
```
#### Functions
- **camelCase** for function names
- **Descriptive action verbs**: `enqueue`, `extractRecipe`, `uploadRecipeImage`
**Examples:**
```typescript
// From QueueManager.ts
enqueue(url: string): QueueItem { ... }
dequeue(): QueueItem | null { ... }
updateStatus(id: string, status: QueueItemStatus): void { ... }
// From extraction.ts
export async function extractTextAndThumbnail(
url: string,
onProgress?: ProgressCallback
): Promise<ExtractedContent> { ... }
// From parser.ts
export async function extractRecipe(text: string): Promise<Recipe> { ... }
```
### Types & Interfaces
#### Interfaces & Types
- **PascalCase** for interface names
- Prefix with `I` is **NOT** used
- Exported types use `export type` or `export interface`
**Examples:**
```typescript
// From queue/types.ts
export interface QueueItem {
id: string;
url: string;
status: QueueItemStatus;
enqueuedAt: string;
// ...
}
export interface QueueStatusUpdate {
type: string;
itemId: string;
status: QueueItemStatus;
// ...
}
export type QueueItemStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
// From extraction.ts
export interface ExtractedContent {
text: string;
thumbnailUrl?: string;
}
export type ProgressCallback = (event: ProgressEvent) => void;
```
#### Zod Schemas
- **PascalCase** with `Schema` suffix
- Inferred types without suffix
**Examples:**
```typescript
// From parser.ts
const RecipeSchema = z.object({
name: z.string(),
description: z.string(),
servings: z.number()
// ...
});
export type Recipe = z.infer<typeof RecipeSchema>;
// From tandoor.ts
const TandoorRecipeSchema = z.object({
// ...
});
export type TandoorRecipe = z.infer<typeof TandoorRecipeSchema>;
```
### Classes
#### Class Names
- **PascalCase** for class names
- Descriptive suffixes: `Manager`, `Service`, `Processor`, `Handler`
**Examples:**
```typescript
// From QueueManager.ts
export class QueueManager {
private items: Map<string, QueueItem> = new Map();
// ...
}
// From QueueProcessor.ts
export class QueueProcessor {
private processing: Set<string> = new Set();
// ...
}
// From PushNotificationService.ts
class PushNotificationService {
private subscriptions: Map<string, PushSubscription> = new Map();
// ...
}
```
#### Singleton Export Pattern
```typescript
// Class definition
export class QueueManager {
// Implementation
}
// Singleton instance export
export const queueManager = new QueueManager();
```
---
## Indentation & Formatting
### General Rules
- **Indentation:** 2 spaces (enforced by Prettier)
- **No tabs**
- **Max line length:** 100 characters (soft limit, not enforced)
- **Trailing commas:** Yes (multiline)
- **Semicolons:** Yes (always)
- **Single quotes:** Yes (enforced by Prettier)
### Code Examples
#### Function Declarations
```typescript
// From QueueManager.ts
enqueue(url: string): QueueItem {
const now = new Date().toISOString();
const item: QueueItem = {
id: uuidv4(),
url,
status: 'pending',
enqueuedAt: now,
createdAt: now,
updatedAt: now,
phases: [
{ name: 'extraction', status: 'pending' },
{ name: 'parsing', status: 'pending' },
{ name: 'uploading', status: 'pending' }
],
logs: [],
progressEvents: [],
retryCount: 0,
maxRetries: 3
};
this.items.set(item.id, item);
return item;
}
```
#### Async Functions
```typescript
// From extraction.ts
export async function extractTextAndThumbnail(
url: string,
onProgress?: ProgressCallback
): Promise<ExtractedContent> {
const browser = await getBrowser();
const context = await createBrowserContext(browser);
const page = await context.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle' });
// ...
} finally {
await context.close();
}
}
```
#### Object Destructuring
```typescript
// From route handlers
export const POST: RequestHandler = async ({ request }) => {
const { url } = await request.json();
// ...
};
export const GET: RequestHandler = async ({ params }) => {
const { id } = params;
// ...
};
```
---
## Import Patterns
### Import Order
1. External dependencies (Node.js built-ins, npm packages)
2. SvelteKit imports (`$lib`, `$app`, `$env`)
3. Relative imports (`./ `, `../`)
4. Type imports (separate from value imports when beneficial)
**Example:**
```typescript
// From QueueProcessor.ts
// External dependencies
import { v4 as uuidv4 } from 'uuid';
// SvelteKit imports
import { queueManager } from './QueueManager';
import { extractTextAndThumbnail } from '$lib/server/extraction';
import { extractRecipe } from '$lib/server/parser';
import { uploadRecipeWithIngredientsDTO, uploadRecipeImage } from '$lib/server/tandoor';
import { pushNotificationService } from '$lib/server/notifications/PushNotificationService';
import { queueConfig } from './config';
// Type imports
import type { ProgressEvent } from '$lib/server/extraction';
import type { QueueItem } from './types';
```
### Import Styles
#### Named Imports (Preferred)
```typescript
import { json } from '@sveltejs/kit';
import { queueManager } from '$lib/server/queue/QueueManager';
import { validateInstagramUrl } from '$lib/server/validation/instagram-url';
```
#### Type-Only Imports
```typescript
import type { RequestHandler } from './$types';
import type { QueueItem, QueueItemStatus } from './types';
```
#### Default Imports
```typescript
import OpenAI from 'openai';
import fs from 'fs';
import path from 'path';
```
### Export Patterns
#### Named Exports (Preferred)
```typescript
// Export functions
export async function extractRecipe(text: string): Promise<Recipe> { ... }
// Export classes
export class QueueManager { ... }
// Export constants
export const queueConfig = { ... };
// Export types
export type Recipe = z.infer<typeof RecipeSchema>;
export interface QueueItem { ... }
```
#### Singleton Pattern Export
```typescript
// Define class
export class QueueManager { ... }
// Export singleton instance
export const queueManager = new QueueManager();
```
---
## Comments & Documentation
### JSDoc Style
Used extensively for public APIs and exported functions.
**Function Documentation:**
````typescript
/**
* Add URL to processing queue
*
* @param url - Instagram URL to process
* @returns Newly created queue item
*
* @example
* ```typescript
* const item = queueManager.enqueue('https://instagram.com/p/abc123');
* console.log('Queued with ID:', item.id);
* ```
*/
enqueue(url: string): QueueItem {
// Implementation
}
````
**Class Documentation:**
````typescript
/**
* Singleton queue manager for processing Instagram URLs
*
* Features:
* - FIFO queue with unique IDs
* - Status tracking and updates
* - Progress event accumulation
* - Retry support for failed items
* - Pub/sub for real-time updates
*
* @example
* ```typescript
* import { queueManager } from './QueueManager';
*
* // Add item to queue
* const item = queueManager.enqueue('https://instagram.com/p/abc123');
* ```
*/
export class QueueManager {
// Implementation
}
````
**Module-Level Documentation:**
```typescript
/**
* Queue Manager - Core queue operations and event management
*
* Manages an in-memory queue of Instagram URL processing jobs.
* Provides CRUD operations and pub/sub mechanism for queue updates.
*
* Architecture: Domain Layer (Hexagonal Architecture)
* - Port: Defines queue operations interface
* - Implementation: In-memory Map-based storage
*/
```
### Inline Comments
#### Single-line Comments
```typescript
// Set restrictive permissions
fs.chmodSync(authFile, 0o600);
// FIFO order - get oldest pending item
const pendingItems = Array.from(this.items.values()).filter((item) => item.status === 'pending');
```
#### Block Comments (Avoided)
Single-line comments preferred. Block comments used only for large comment blocks or temporarily disabling code during development.
### TODO Comments
```typescript
// TODO: Add retry logic with exponential backoff
// FIXME: Handle race condition when multiple workers dequeue
```
---
## TypeScript Patterns
### Type Safety
#### Strict Mode Enabled
```json
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"forceConsistentCasingInFileNames": true
}
}
```
#### Type Annotations
```typescript
// Explicit return types for public functions
export async function extractRecipe(text: string): Promise<Recipe> { ... }
// Explicit parameter types
function processItem(item: QueueItem, config: QueueConfig): void { ... }
// Type inference for local variables (acceptable)
const items = queueManager.getAll(); // Type inferred
```
### Union Types
```typescript
export type QueueItemStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
export type ProcessingPhase = 'extraction' | 'parsing' | 'uploading';
export type ProgressEventType = 'status' | 'method' | 'retry' | 'error' | 'thumbnail' | 'complete';
```
### Generics
```typescript
// Generic function
async function fetchFromTandoor<T>(
url: string,
options: Partial<RequestInit> = { method: 'GET' }
): Promise<{ ok: boolean; data?: T; error?: string }> {
// Implementation
}
```
---
## Svelte 5 Patterns
### Runes (Reactivity)
#### $state (Reactive Variables)
```svelte
<script lang="ts">
let count = $state(0);
let items = $state<QueueItem[]>([]);
</script>
```
#### $props (Component Props)
```svelte
<script lang="ts">
let {
recipe = null,
tandoorEnabled = false,
onRetry,
onImportToTandoor
} = $props<{
recipe: Recipe | null;
tandoorEnabled: boolean;
onRetry: () => void;
onImportToTandoor: () => void;
}>();
</script>
```
#### $derived (Computed Values)
```svelte
<script lang="ts">
let count = $state(0);
let double = $derived(count * 2);
</script>
```
#### $effect (Side Effects)
```svelte
<script lang="ts">
let url = $state('');
$effect(() => {
console.log('URL changed:', url);
});
</script>
```
### Component Structure
```svelte
<script lang="ts">
// Imports
import { onMount } from 'svelte';
// Props
let { items } = $props<{ items: Item[] }>();
// State
let loading = $state(false);
// Derived state
let count = $derived(items.length);
// Functions
function handleClick() {
// ...
}
// Effects
$effect(() => {
// Side effects
});
</script>
<!-- Template -->
<div>
<!-- Markup -->
</div>
<!-- Styles -->
<style>
/* Component-scoped styles */
</style>
```
---
## Error Handling
### Custom Error Classes
```typescript
// From api/errors.ts
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
export class NotFoundError extends Error {
constructor(resource: string) {
super(`${resource} not found`);
this.name = 'NotFoundError';
}
}
export class ConflictError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConflictError';
}
}
```
### Try-Catch Pattern
```typescript
export const POST: RequestHandler = async ({ request }) => {
try {
const { url } = await request.json();
if (!url) {
throw new ValidationError('URL is required');
}
const item = queueManager.enqueue(url);
return json(item, { status: 201 });
} catch (error) {
return handleApiError(error);
}
};
```
---
## Linting Configuration
### ESLint
**Config:** `eslint.config.js`
- Base: `@eslint/js` recommended
- TypeScript: `typescript-eslint` recommended
- Svelte: `eslint-plugin-svelte` recommended
- Formatting: `eslint-config-prettier`
**Rules:**
```javascript
{
rules: {
"no-undef": 'off' // TypeScript handles this
}
}
```
### Prettier
**Config:** `.prettierrc`
```json
{
"useTabs": false,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"]
}
```
---
## Testing Conventions
### Test Structure
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('QueueManager', () => {
let manager: QueueManager;
beforeEach(() => {
manager = new QueueManager();
});
it('should enqueue items', () => {
const item = manager.enqueue('https://instagram.com/p/test');
expect(item.status).toBe('pending');
});
it('should dequeue items in FIFO order', () => {
manager.enqueue('url1');
manager.enqueue('url2');
const first = manager.dequeue();
expect(first?.url).toBe('url1');
});
});
```
### Mock Pattern
```typescript
vi.mock('$lib/server/extraction', () => ({
extractTextAndThumbnail: vi.fn().mockResolvedValue({
text: 'Mock text',
thumbnailUrl: 'https://example.com/thumb.jpg'
})
}));
```
---
## File Headers
### Module Documentation Pattern
Every major module includes a header comment:
```typescript
/**
* Module Name - Brief Description
*
* Detailed description of the module's purpose and functionality.
*
* Architecture: Layer Name (Hexagonal Architecture)
* - Port: Description of port interface
* - Implementation: Description of concrete implementation
*/
```
**Example:**
```typescript
/**
* Queue Manager - Core queue operations and event management
*
* Manages an in-memory queue of Instagram URL processing jobs.
* Provides CRUD operations and pub/sub mechanism for queue updates.
*
* Architecture: Domain Layer (Hexagonal Architecture)
* - Port: Defines queue operations interface
* - Implementation: In-memory Map-based storage
*/
```
---
## Additional Conventions
### Environment Variables
```typescript
import { env } from '$env/dynamic/private';
const apiKey = env.OPENAI_API_KEY;
const tandoorUrl = env.TANDOOR_URL || null;
```
### Date Handling
ISO8601 strings throughout the application:
```typescript
const now = new Date().toISOString();
// Output: "2026-02-15T12:30:45.123Z"
```
### Null vs Undefined
- `null`: Intentional absence of value
- `undefined`: Not yet initialized or optional parameters
- Prefer `null` for API responses and data structures
### Async/Await
Always preferred over Promise chains:
```typescript
// Preferred
async function fetchData() {
const response = await fetch(url);
const data = await response.json();
return data;
}
// Avoid
function fetchData() {
return fetch(url)
.then((response) => response.json())
.then((data) => data);
}
```
---
**Document Version:** 1.0
**Enforced By:** ESLint, Prettier, TypeScript
**Generated by:** Initializer Agent