|
1 | 1 | # Agent Guidelines for Annum |
2 | 2 |
|
3 | | -This document provides coding agents with essential information about the Annum codebase structure, conventions, and workflows. |
| 3 | +This document provides essential information for AI coding agents working on this codebase. |
4 | 4 |
|
5 | 5 | ## Project Overview |
6 | 6 |
|
7 | | -Annum is a SvelteKit application that visualizes Trakt.tv watch history. It uses: |
8 | | -- **SvelteKit** (v5) with TypeScript |
9 | | -- **Vite** for bundling |
10 | | -- **Vitest** for testing |
11 | | -- **pnpm** as package manager |
12 | | -- **Auth.js** (@auth/sveltekit) for authentication |
13 | | -- **Netlify** for deployment |
| 7 | +Annum is a SvelteKit application that visualizes Trakt.tv watch history. It uses Svelte 5, TypeScript, Better Auth for authentication, and is deployed to Netlify. |
14 | 8 |
|
15 | | -## Build, Test & Lint Commands |
| 9 | +## Build & Development Commands |
16 | 10 |
|
| 11 | +### Development |
17 | 12 | ```bash |
18 | | -# Development |
19 | | -pnpm dev # Start dev server on port 5173 |
20 | | -pnpm build # Create production build |
21 | | -pnpm preview # Preview production build locally |
22 | | - |
23 | | -# Type Checking & Linting |
24 | | -pnpm check # Run svelte-check for type errors |
25 | | -pnpm check:watch # Run svelte-check in watch mode |
26 | | -pnpm typecheck # Run TypeScript compiler without emitting files |
27 | | -pnpm lint # Run ESLint |
28 | | -pnpm lint:fix # Run ESLint with auto-fix |
29 | | - |
30 | | -# Testing |
31 | | -pnpm test # Run tests in watch mode |
32 | | -pnpm test:ci # Run tests once (for CI) |
33 | | -pnpm test:coverage # Run tests with coverage report |
| 13 | +pnpm dev # Start development server |
| 14 | +pnpm preview # Preview production build locally |
| 15 | +``` |
| 16 | + |
| 17 | +### Building |
| 18 | +```bash |
| 19 | +pnpm build # Create production build |
| 20 | +``` |
| 21 | + |
| 22 | +### Code Quality |
| 23 | +```bash |
| 24 | +pnpm check # Run svelte-check for type checking |
| 25 | +pnpm check:watch # Run svelte-check in watch mode |
| 26 | +pnpm lint # Run ESLint |
| 27 | +pnpm lint:fix # Run ESLint with auto-fix |
| 28 | +pnpm typecheck # Run TypeScript type checking |
| 29 | +``` |
| 30 | + |
| 31 | +### Testing |
| 32 | +```bash |
| 33 | +pnpm test # Run tests in watch mode |
| 34 | +pnpm test:ci # Run tests once (CI mode) |
| 35 | +pnpm test:coverage # Run tests with coverage report |
34 | 36 |
|
35 | 37 | # Run a single test file |
36 | | -pnpm vitest src/lib/utils/__tests__/index.ts |
| 38 | +pnpm vitest run src/lib/utils/__tests__/index.ts |
37 | 39 |
|
38 | | -# Run a single test by name pattern |
39 | | -pnpm vitest -t "chunks" |
| 40 | +# Run a single test file in watch mode |
| 41 | +pnpm vitest watch src/lib/utils/__tests__/index.ts |
40 | 42 | ``` |
41 | 43 |
|
| 44 | +**Test Configuration:** |
| 45 | +- Test files: `src/**/__tests__/*.ts` |
| 46 | +- Coverage includes: `src/lib/utils/*.ts` and `src/lib/actions.ts` |
| 47 | +- Test environment: happy-dom |
| 48 | +- Framework: vitest |
| 49 | + |
42 | 50 | ## Code Style Guidelines |
43 | 51 |
|
44 | | -### ESLint Configuration |
| 52 | +### General Formatting |
45 | 53 |
|
46 | | -The project uses `@antfu/eslint-config` with custom overrides: |
| 54 | +**ESLint Config:** Uses `@antfu/eslint-config` with customizations |
47 | 55 |
|
48 | | -- **Indentation**: Tabs (not spaces) |
49 | | -- **Quotes**: Single quotes (avoid escape when necessary) |
50 | | -- **Semicolons**: No semicolons |
51 | | -- **Array Types**: Use generic syntax `Array<T>` not `T[]` |
| 56 | +- **Indentation:** Tabs (not spaces) |
| 57 | +- **Quotes:** Single quotes (use `avoidEscape: true` for strings with single quotes) |
| 58 | +- **Semicolons:** No semicolons |
| 59 | +- **Line breaks:** LF (Unix-style) |
52 | 60 |
|
53 | 61 | ### TypeScript |
54 | 62 |
|
55 | | -- **Strict mode enabled**: All strict TypeScript checks are on |
56 | | -- **No `{}` or `object` types**: Use `Record<string, unknown>` instead |
57 | | -- **Unused variables**: Prefix with underscore `_` to indicate intentionally unused (e.g., `_variable`, `_arg`) |
58 | | -- **Type imports**: Use `import type` for type-only imports |
59 | | -- **No `any`**: Avoid using `any` type; use proper types or `unknown` |
60 | | - |
61 | | -### Naming Conventions |
| 63 | +**Strict mode enabled** - All TypeScript strict checks are enforced |
62 | 64 |
|
63 | | -- **Files**: Use kebab-case for files (e.g., `user-stats.ts`) |
64 | | -- **Components**: PascalCase for Svelte components (e.g., `Primary.svelte`) |
65 | | -- **Functions**: camelCase for functions (e.g., `normalizeItem`) |
66 | | -- **Constants**: SCREAMING_SNAKE_CASE for top-level constants (e.g., `TRAKT_BASE_URL`) |
67 | | -- **Types/Interfaces**: PascalCase (e.g., `TraktHistoryItem`) |
| 65 | +**Type Definitions:** |
| 66 | +- Use explicit type annotations for function parameters and return types |
| 67 | +- Prefer `interface` over `type` for object shapes (allows `with-single-extends`) |
| 68 | +- Use `Array<T>` generic syntax instead of `T[]` |
| 69 | +- Never use `{}` or `object` - use `Record<string, unknown>` instead |
| 70 | +- No wrapper object types (`String`, `Number`, etc.) |
68 | 71 |
|
69 | | -### Imports |
| 72 | +**Type Imports:** |
| 73 | +- Always use `type` keyword for type-only imports: |
| 74 | + ```typescript |
| 75 | + import type { Language, NormalizedItemResponse } from '$lib/types' |
| 76 | + import type { PageData } from './$types' |
| 77 | + ``` |
70 | 78 |
|
71 | | -Order imports by: |
72 | | -1. Type imports (using `import type`) |
73 | | -2. External dependencies |
74 | | -3. Internal modules (using SvelteKit aliases) |
| 79 | +**Unused Variables:** |
| 80 | +- Prefix unused variables with underscore: `_variableName` or just `_` |
| 81 | +- Applies to function args, variables, and caught errors |
75 | 82 |
|
76 | | -Example: |
| 83 | +### Naming Conventions |
77 | 84 |
|
78 | | -```typescript |
79 | | -import type { Language, NormalizedItemResponse } from '$lib/types' |
80 | | -import { page } from '$app/state' |
81 | | -import { normalizeItem } from '$lib/utils' |
82 | | -import { signIn } from '@auth/sveltekit/client' |
83 | | -``` |
| 85 | +- **Files:** kebab-case for most files (e.g., `custom-media-queries.css`) |
| 86 | +- **Components:** PascalCase for Svelte components (e.g., `Secondary.svelte`) |
| 87 | +- **Functions:** camelCase (e.g., `normalizeItem`, `filterForYear`) |
| 88 | +- **Constants:** SCREAMING_SNAKE_CASE (e.g., `PAGINATION_LIMIT`, `TRAKT_BASE_URL`) |
| 89 | +- **Types/Interfaces:** PascalCase (e.g., `TraktHistoryItem`, `NormalizedItemResponse`) |
84 | 90 |
|
85 | | -### SvelteKit Path Aliases |
| 91 | +### Imports |
86 | 92 |
|
87 | | -- `$lib/*` → `src/lib/*` |
| 93 | +**Path Aliases:** |
| 94 | +- `$lib` → `src/lib` |
88 | 95 | - `$assets` → `src/assets` |
89 | 96 | - `$const` → `src/const.ts` |
90 | | -- `$app/*` → SvelteKit internals (state, navigation, stores, etc.) |
| 97 | +- Use these aliases consistently instead of relative paths |
91 | 98 |
|
92 | | -### Environment Variables |
93 | | - |
94 | | -- All private environment variables must use `PRIVATE_` prefix (configured in `svelte.config.js`) |
95 | | -- Example: `PRIVATE_TRAKT_CLIENT_ID`, `PRIVATE_AUTH_SECRET` |
96 | | - |
97 | | -## Function Documentation |
98 | | - |
99 | | -All utility functions should include JSDoc comments with: |
100 | | -- Description of what the function does |
101 | | -- `@example` tag showing usage |
| 99 | +**Import Order:** |
| 100 | +1. Type imports |
| 101 | +2. External dependencies |
| 102 | +3. Internal modules (using path aliases) |
| 103 | +4. Relative imports |
102 | 104 |
|
103 | 105 | Example: |
104 | | - |
105 | 106 | ```typescript |
106 | | -/** |
107 | | - * Split an array into chunks of a given size |
108 | | - * @example chunks([1, 2, 3, 4, 5], 2) => [[1, 2], [3, 4], [5]] |
109 | | - */ |
110 | | -export function chunks<T>(array: Array<T>, number: number | string): Array<Array<T>> |
| 107 | +import type { Language, TraktMediaType } from '$lib/types' |
| 108 | +import type { RequestHandler } from './$types' |
| 109 | +import { DEFAULT_CACHE_HEADER, PAGINATION_LIMIT } from '$const' |
| 110 | +import { normalizeItem } from '$lib/utils' |
| 111 | +import { error, json } from '@sveltejs/kit' |
111 | 112 | ``` |
112 | 113 |
|
113 | | -## Error Handling |
114 | | - |
115 | | -- Use SvelteKit's `error()` helper for HTTP errors |
116 | | -- Include helpful error messages with context |
117 | | -- Log warnings to console for non-critical issues (e.g., missing TMDB IDs) |
| 114 | +### Svelte 5 Conventions |
118 | 115 |
|
119 | | -Example: |
| 116 | +**Props:** |
120 | 117 | ```typescript |
121 | | -if (!session?.user) |
122 | | - error(401, 'You must sign in to access this route.') |
123 | | -``` |
124 | | - |
125 | | -## Testing Patterns |
| 118 | +interface Props { |
| 119 | + data: PageData |
| 120 | +} |
126 | 121 |
|
127 | | -- **Test files**: Located in `__tests__/` directories alongside source files |
128 | | -- **Fixtures**: Store test data in `__fixtures__/` directories |
129 | | -- **File pattern**: `src/**/__tests__/*.ts` |
130 | | -- **Coverage**: Includes `src/lib/utils/*.ts` and `src/lib/actions.ts` |
| 122 | +let { data }: Props = $props() |
| 123 | +``` |
131 | 124 |
|
132 | | -Test structure: |
| 125 | +**Reactivity:** |
| 126 | +- Use `$state` for reactive variables |
| 127 | +- Use `$derived` for computed values |
| 128 | +- Use `$effect` for side effects |
133 | 129 |
|
| 130 | +**Store Usage:** |
134 | 131 | ```typescript |
135 | | -import { describe, expect, it } from 'vitest' |
136 | | -
|
137 | | -describe('functionName', () => { |
138 | | - it('should describe expected behavior', () => { |
139 | | - const result = functionName(input) |
140 | | - expect(result).toBe(expected) |
141 | | - }) |
142 | | -}) |
| 132 | +import { settings } from '$lib/store/settings' |
| 133 | + |
| 134 | +// Access with $ |
| 135 | +$settings.hue |
| 136 | +settings.set({ ...$settings, hue: 240 }) |
143 | 137 | ``` |
144 | 138 |
|
145 | | -## Svelte 5 Patterns |
| 139 | +### CSS/Styling |
| 140 | + |
| 141 | +**PostCSS:** Uses `postcss-preset-env` with custom media queries |
146 | 142 |
|
147 | | -- Use Svelte 5 runes: `$state`, `$derived`, `$effect`, `$props` |
148 | | -- Access page store with `page` from `$app/state` |
149 | | -- Use `<script lang='ts'>` for TypeScript in Svelte files |
150 | | -- For PostCSS in styles: `<style lang='postcss'>` |
| 143 | +**Custom Media Queries:** |
| 144 | +- `--sm` (min-width: 640px) |
| 145 | +- `--md` (min-width: 768px) |
| 146 | +- `--lg` (min-width: 1024px) |
| 147 | +- `--xl` (min-width: 1350px) |
151 | 148 |
|
152 | | -## API Routes |
| 149 | +**Usage:** |
| 150 | +```css |
| 151 | +.element { |
| 152 | + display: block; |
153 | 153 |
|
154 | | -- API routes in `src/routes/api/` |
155 | | -- Use `RequestHandler` type from `./$types` |
156 | | -- Set cache headers using `setHeaders()` |
157 | | -- Return responses with `json()` helper |
158 | | -- Check authentication with `await locals.auth()` |
| 154 | + @media (--md) { |
| 155 | + display: flex; |
| 156 | + } |
| 157 | +} |
| 158 | +``` |
159 | 159 |
|
160 | | -Example structure: |
| 160 | +**CSS Variables:** Project uses extensive CSS custom properties defined in `src/styles/variables.css` |
161 | 161 |
|
162 | | -```typescript |
163 | | -export const GET: RequestHandler = async ({ locals, url, fetch, setHeaders }) => { |
164 | | - const session = await locals.auth() |
165 | | - if (!session?.user) |
166 | | - error(401, 'You must sign in to access this route.') |
| 162 | +### Error Handling |
167 | 163 |
|
168 | | - setHeaders({ ...DEFAULT_CACHE_HEADER }) |
| 164 | +**Server Routes:** |
| 165 | +```typescript |
| 166 | +// Use SvelteKit's error helper |
| 167 | +if (!user) |
| 168 | + error(401, 'You must sign in to access this route.') |
169 | 169 |
|
170 | | - // Implementation |
171 | | - return json({ data }) |
| 170 | +// Try-catch for async operations |
| 171 | +try { |
| 172 | + const res = await fetch(url) |
| 173 | + if (!res.ok) |
| 174 | + throw new Error(`Response not OK: ${res.status}`) |
| 175 | + // ... handle response |
| 176 | +} |
| 177 | +catch (e) { |
| 178 | + error(404, `Failed to fetch data. ${e}`) |
172 | 179 | } |
173 | 180 | ``` |
174 | 181 |
|
175 | | -## Constants |
| 182 | +**Logging:** |
| 183 | +- Use `console.warn()` for non-fatal issues |
| 184 | +- Include context in log messages (IDs, types, titles) |
176 | 185 |
|
177 | | -Define all constants in `src/const.ts` using: |
178 | | -- `as const` for literal types |
179 | | -- `satisfies` for type checking without widening |
| 186 | +### Documentation |
180 | 187 |
|
181 | | -Example: |
| 188 | +**JSDoc Comments:** |
| 189 | +- Add JSDoc for utility functions |
| 190 | +- Include `@example` usage examples |
| 191 | +- Document parameters and return types |
182 | 192 |
|
| 193 | +Example: |
183 | 194 | ```typescript |
184 | | -export const TMDB_FETCH_DEFAULTS = { |
185 | | - method: 'GET', |
186 | | - headers: { 'user-agent': 'annum' }, |
187 | | -} satisfies RequestInit |
| 195 | +/** |
| 196 | + * Split an array into chunks of a given size |
| 197 | + * @example chunks([1, 2, 3, 4, 5], 2) => [[1, 2], [3, 4], [5]] |
| 198 | + */ |
| 199 | +export function chunks<T>(array: Array<T>, number: number | string): Array<Array<T>> |
188 | 200 | ``` |
189 | 201 |
|
190 | | -## Common Patterns |
| 202 | +## Environment Variables |
191 | 203 |
|
192 | | -### Type Guards |
| 204 | +**Private Variables:** Prefix with `PRIVATE_` (configured in `svelte.config.js`) |
| 205 | +- `PRIVATE_TRAKT_CLIENT_ID` |
| 206 | +- `PRIVATE_TRAKT_CLIENT_SECRET` |
| 207 | +- `PRIVATE_AUTH_SECRET` |
| 208 | +- `PRIVATE_TMDB_API_KEY` |
193 | 209 |
|
| 210 | +**Public Variables:** Prefix with `PUBLIC_` |
| 211 | +- `PUBLIC_BETTER_AUTH_URL` |
| 212 | + |
| 213 | +## Common Patterns |
| 214 | + |
| 215 | +**Type Guards:** |
194 | 216 | ```typescript |
195 | 217 | function isTraktWatchedItem(item: TraktHistoryItem | TraktWatchedItem): item is TraktWatchedItem { |
196 | 218 | return !('type' in item) |
197 | 219 | } |
198 | 220 | ``` |
199 | 221 |
|
200 | | -### Generic Utility Types |
201 | | - |
202 | | -```typescript |
203 | | -type Filter<T> = MapValuesToKeysIfAllowed<T>[keyof T] |
204 | | -export function groupBy<T extends Record<PropertyKey, any>, Key extends Filter<T>>( |
205 | | - arr: Array<T>, |
206 | | - key: Key, |
207 | | -): Record<T[Key], Array<T>> |
208 | | -``` |
209 | | - |
210 | | -## Git & Deployment |
211 | | - |
212 | | -- Deployed on Netlify (adapter: `@sveltejs/adapter-netlify`) |
213 | | -- Redirects configured in `_redirects` file |
214 | | -- Site manifest: `static/site.webmanifest` |
215 | | - |
216 | | -## Additional Notes |
| 222 | +**API Responses:** |
| 223 | +- Set cache headers with `setHeaders()` |
| 224 | +- Return JSON with SvelteKit's `json()` helper |
| 225 | +- Use URL search params for query parameters |
217 | 226 |
|
218 | | -- Use `enhancedImages()` from `@sveltejs/enhanced-img` for optimized images |
219 | | -- Custom media queries defined in `src/styles/custom-media-queries.css` |
220 | | -- CSS variables in `src/styles/variables.css` |
221 | | -- Reset styles in `src/styles/reset.css` |
| 227 | +**Authentication:** |
| 228 | +- Uses Better Auth with stateless JWT sessions |
| 229 | +- Check `locals.user` for authenticated user |
| 230 | +- Trakt OAuth for provider authentication |
0 commit comments