833 lines
16 KiB
Markdown
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
|