Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 33 additions & 211 deletions docs/architecture/frontend-build.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
# Frontend build system

This document explains how the frontend codebase is built, what libraries are involved, and how different parts connect at compile time and runtime. It's written for developers who need to modify the build pipeline or understand how the frontend works.

## Overview

The frontend is a Svelte 5 single-page application bundled with Rollup. It uses TypeScript for type safety, Tailwind CSS v4 for styling, and a generated SDK for type-safe API calls. The build outputs static files to `public/build/` which are served by nginx in production or by a custom HTTPS dev server during development.

For details on the Svelte 5 runes API and migration patterns, see [Svelte 5 Migration](svelte5-migration.md).
The frontend is a Svelte 5 single-page application bundled with Rollup. It uses TypeScript for type safety, Tailwind CSS v4 for styling, and a generated SDK for type-safe API calls. The build outputs static files to `public/build/` which are served by nginx in production or by a custom HTTPS dev server during development. For details on the Svelte 5 runes API and migration patterns, see [Svelte 5 Migration](svelte5-migration.md).

```mermaid
graph LR
Expand Down Expand Up @@ -44,11 +38,7 @@ graph LR

## Rollup configuration

The `rollup.config.js` file configures the entire build pipeline. It produces ES modules with code splitting, enabling parallel loading of vendor code and application code.

### Entry point

The build starts from `src/main.ts`, which imports the API client setup, mounts the Svelte `App` component using Svelte 5's `mount()` function, and imports global CSS:
The `rollup.config.js` file configures the entire build pipeline, producing ES modules with code splitting for parallel loading of vendor and application code. The build starts from `src/main.ts`, which imports the API client setup, mounts the Svelte `App` component using Svelte 5's `mount()` function, and imports global CSS:

```typescript
import { mount } from 'svelte';
Expand All @@ -61,57 +51,20 @@ const app = mount(App, {
});
```

### Code splitting

Rollup splits the bundle into chunks to improve load performance. The `manualChunks` configuration separates large dependencies:

| Chunk | Contents |
|-------|----------|
| `vendor` | Svelte, @mateothegreat/svelte5-router |
| `codemirror` | All CodeMirror packages for the editor |
| Application chunks | Route components and shared code |

This means users don't re-download vendor code when application code changes, and the editor chunk only loads when needed.

### Plugins

The plugin pipeline processes files in order:
Rollup splits the bundle into chunks to improve load performance. The `manualChunks` configuration separates large dependencies into a `vendor` chunk (Svelte, svelte5-router), a `codemirror` chunk (all CodeMirror packages), and application chunks for routes and shared code. This means users don't re-download vendor code when application code changes, and the editor chunk only loads when needed.

1. **replace** — Substitutes `process.env.VITE_BACKEND_URL` with an empty string, allowing relative API paths
2. **svelte** — Compiles `.svelte` files with TypeScript preprocessing via `svelte-preprocess`, with `runes: true` enabled for Svelte 5
3. **postcss** — Processes CSS through PostCSS, extracting styles to `bundle.css`
4. **typescript** — Compiles TypeScript files with source maps
5. **json** — Allows importing JSON files
6. **resolve** — Resolves `node_modules` imports for browser usage, preferring ES modules
7. **commonjs** — Converts CommonJS modules to ES modules
8. **terser** (production only) — Minifies JavaScript, removes console logs, runs two compression passes
The plugin pipeline processes files in order: **replace** substitutes `process.env.VITE_BACKEND_URL` with an empty string for relative API paths; **svelte** compiles `.svelte` files with TypeScript preprocessing and `runes: true` for Svelte 5; **postcss** processes CSS and extracts styles to `bundle.css`; **typescript** compiles TypeScript with source maps; **json** allows importing JSON files; **resolve** handles `node_modules` imports preferring ES modules; **commonjs** converts CommonJS to ES modules; and **terser** (production only) minifies JavaScript, removes console logs, and runs two compression passes.

### Development server

In development mode (`npm run dev`), Rollup watches for changes and a custom HTTPS server starts automatically. The server handles two responsibilities:

1. **Static file serving** — Serves files from `public/`, falling back to `index.html` for SPA routing
2. **API proxying** — Forwards `/api/*` requests to the backend container over HTTPS

The proxy uses a custom `https.Agent` that trusts the local CA certificate at `/shared_ca/mkcert-ca.pem`, allowing secure communication with the backend during development. The server listens on port 5001.
In development mode (`npm run dev`), Rollup watches for changes and a custom HTTPS server starts automatically. The server serves files from `public/` with SPA fallback to `index.html`, and proxies `/api/*` requests to the backend container over HTTPS. The proxy uses a custom `https.Agent` that trusts the local CA certificate at `/shared_ca/mkcert-ca.pem`, allowing secure communication with the backend during development on port 5001.

## TypeScript configuration

The `tsconfig.json` configures TypeScript compilation:

- Target: ES2020 with ESNext modules
- Strict mode enabled
- Module resolution set to bundler mode for Rollup compatibility
- Svelte component types enabled via `svelte-preprocess`

TypeScript catches type errors during development and the build fails if any exist, preventing broken code from reaching production.
The `tsconfig.json` targets ES2020 with ESNext modules, enables strict mode, sets module resolution to bundler mode for Rollup compatibility, and enables Svelte component types via `svelte-preprocess`. TypeScript catches type errors during development and the build fails if any exist, preventing broken code from reaching production.

## API SDK generation

The frontend uses a generated SDK for type-safe API calls instead of manual fetch requests. This SDK is created from the backend's OpenAPI specification using `@hey-api/openapi-ts`.

### Generation pipeline

```mermaid
graph LR
OpenAPI["docs/reference/openapi.json"] --> Generator["@hey-api/openapi-ts"]
Expand All @@ -120,30 +73,12 @@ graph LR
Generator --> Client["client.gen.ts"]
```

Run `npm run generate:api` to regenerate the SDK. The configuration in `openapi-ts.config.ts` specifies:

- Input: `../docs/reference/openapi.json` (the backend's OpenAPI spec)
- Output: `src/lib/api/` with Prettier formatting
- Plugins: TypeScript types, SDK functions, and fetch client

### Generated files

| File | Purpose |
|------|---------|
| `types.gen.ts` | TypeScript interfaces for all request/response models |
| `sdk.gen.ts` | Function for each API endpoint, fully typed |
| `client.gen.ts` | HTTP client with interceptor support |
| `index.ts` | Re-exports types and SDK functions |
| `setup.ts` | Manual file that configures the client (not generated) |

### Client configuration

The `setup.ts` file configures the generated client:
Run `npm run generate:api` to regenerate the SDK. The configuration in `openapi-ts.config.ts` specifies the input as `../docs/reference/openapi.json`, outputs to `src/lib/api/` with Prettier formatting, and generates TypeScript types, SDK functions, and a fetch client. The generated files include `types.gen.ts` (TypeScript interfaces for all request/response models), `sdk.gen.ts` (functions for each API endpoint, fully typed), `client.gen.ts` (HTTP client with interceptor support), and `index.ts` (re-exports for convenient imports). There's also a manual `setup.ts` file that configures the client with relative URLs and cookie credentials, and adds an interceptor that automatically attaches CSRF tokens to mutating requests:

```typescript
client.setConfig({
baseUrl: '', // Relative URLs, proxied in dev
credentials: 'include', // Send cookies for auth
baseUrl: '',
credentials: 'include',
});

client.interceptors.request.use((request) => {
Expand All @@ -155,121 +90,50 @@ client.interceptors.request.use((request) => {
});
```

The interceptor automatically adds CSRF tokens to mutating requests, pulling the token from the auth store. This happens transparently for all SDK calls.

### Usage pattern

Components import SDK functions and types directly:
Components import SDK functions and types directly. The SDK returns `{ data, error }` tuples, making error handling explicit without try/catch boilerplate:

```typescript
import {
getNotificationsApiV1NotificationsGet,
type NotificationResponse,
} from '../lib/api';
import { getNotificationsApiV1NotificationsGet, type NotificationResponse } from '../lib/api';

const { data, error } = await getNotificationsApiV1NotificationsGet({
query: { limit: 20 }
});
const { data, error } = await getNotificationsApiV1NotificationsGet({ query: { limit: 20 } });
```

The SDK returns `{ data, error }` tuples, making error handling explicit without try/catch boilerplate.

## Tailwind CSS v4

The frontend uses Tailwind CSS v4 with the new CSS-first configuration. Unlike v3, there's no `tailwind.config.js` — all configuration lives in CSS.

### PostCSS integration

PostCSS processes CSS through `@tailwindcss/postcss`:

```javascript
// postcss.config.cjs
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
}
```

### CSS configuration

The `src/app.css` file contains all Tailwind configuration using v4's new at-rules:
The frontend uses Tailwind CSS v4 with the new CSS-first configuration. Unlike v3, there's no `tailwind.config.js` — all configuration lives in CSS. PostCSS processes CSS through `@tailwindcss/postcss`, and the `src/app.css` file contains all Tailwind configuration using v4's new at-rules:

```css
/* Import Tailwind */
@import "tailwindcss";

/* Forms plugin */
@plugin "@tailwindcss/forms" {
strategy: class;
}

/* Class-based dark mode */
@plugin "@tailwindcss/forms" { strategy: class; }
@variant dark (&:where(.dark, .dark *));

/* Custom theme tokens */
@theme {
--color-primary: #3b82f6;
--color-bg-default: #f8fafc;
--font-sans: 'Inter', ui-sans-serif, system-ui;
/* ... */
}

/* Custom utilities */
@utility animate-fadeIn {
animation: fadeIn 0.3s ease-in-out;
animation: var(--animate-fadeIn);
}
```

### Theme structure

The theme defines semantic color tokens for both light and dark modes:

| Token | Light | Dark |
|-------|-------|------|
| `bg-default` | `#f8fafc` | `#0f172a` |
| `fg-default` | `#1e293b` | `#e2e8f0` |
| `border-default` | `#e2e8f0` | `#334155` |

Components use these tokens (e.g., `bg-bg-default dark:bg-dark-bg-default`) for consistent theming. The `@variant dark` rule enables the `.dark` class on `<html>` to trigger dark mode.
The theme defines semantic color tokens for both light and dark modes (`bg-default`, `fg-default`, `border-default`, etc.) which components use for consistent theming. The `@variant dark` rule enables the `.dark` class on `<html>` to trigger dark mode. Styles are organized into Tailwind layers: **base** for element defaults, form styles, scrollbars, and CodeMirror overrides; **components** for reusable patterns like `.btn`, `.card`, and `.form-input-standard`.

### Layer organization
## Svelte stores and state

Styles are organized into Tailwind layers:
The frontend uses a hybrid approach to state management. Svelte stores in `src/stores/` handle global, shared state: `auth.ts` manages authentication state, login/logout, and CSRF tokens; `theme.ts` handles theme preference with localStorage persistence; `toastStore.ts` manages the toast notification queue; and `notificationStore.ts` handles server notifications with pagination. Stores use the generated SDK for API calls and persist state to localStorage where appropriate. The auth store exposes a `csrfToken` store that the API client interceptor reads for request signing.

- **base** — Element defaults, form styles, scrollbars, CodeMirror overrides
- **components** — Reusable patterns like `.btn`, `.card`, `.form-input-standard`

## Svelte stores and runes

The frontend uses a hybrid approach to state management:

**Svelte stores** (`src/stores/`) handle global, shared state:

| Store | Purpose |
|-------|---------|
| `auth.ts` | Authentication state, login/logout, CSRF token |
| `theme.ts` | Theme preference (light/dark/auto) with localStorage persistence |
| `toastStore.ts` | Toast notifications queue |
| `notificationStore.ts` | Server notifications with pagination |

Stores use the generated SDK for API calls and persist state to localStorage where appropriate. The auth store exposes a `csrfToken` store that the API client interceptor reads for request signing.

**Svelte 5 runes** (`$state`, `$derived`, `$effect`) handle component-local state:
Svelte 5 runes (`$state`, `$derived`, `$effect`) handle component-local state. Store subscriptions remain unchanged from Svelte 4 — the `$storeName` syntax auto-subscribes to any store:

```svelte
<script lang="ts">
import { isAuthenticated } from '../stores/auth';

// Component-local reactive state
let loading = $state(false);
let items = $state<Item[]>([]);

// Derived values
let itemCount = $derived(items.length);

// Store subscription (unchanged from Svelte 4)
// $isAuthenticated auto-subscribes to the store
</script>

{#if $isAuthenticated}
Expand All @@ -279,14 +143,6 @@ Stores use the generated SDK for API calls and persist state to localStorage whe

For detailed patterns and migration guidance, see [Svelte 5 Migration](svelte5-migration.md).

## Build commands

| Command | Purpose |
|---------|---------|
| `npm run dev` | Start Rollup in watch mode with HTTPS dev server |
| `npm run build` | Production build with minification |
| `npm run generate:api` | Regenerate SDK from OpenAPI spec |

## File structure

```
Expand All @@ -313,64 +169,30 @@ frontend/
└── openapi-ts.config.ts # SDK generator config
```

## Local development

Start the development stack:

```bash
# From project root
docker compose up -d

# In frontend directory
npm install
npm run dev
```

The dev server runs at `https://localhost:5001`. API requests proxy to the backend container. Changes to `.svelte`, `.ts`, and `.css` files trigger automatic rebuilds.

### Regenerating the API client
## Build commands

When backend endpoints change:
| Command | Purpose |
|---------|---------|
| `npm run dev` | Start Rollup in watch mode with HTTPS dev server |
| `npm run build` | Production build with minification |
| `npm run generate:api` | Regenerate SDK from OpenAPI spec |

1. Update the backend and restart it
2. Fetch the new OpenAPI spec (the docs workflow does this automatically)
3. Run `npm run generate:api`
4. Fix any TypeScript errors from changed types
## Local development

### Adding new routes
Start the development stack from the project root with `docker compose up -d`, then in the frontend directory run `npm install && npm run dev`. The dev server runs at `https://localhost:5001` with API requests proxying to the backend container. Changes to `.svelte`, `.ts`, and `.css` files trigger automatic rebuilds.

1. Create a component in `src/routes/`
2. Add a `<Route>` entry in `App.svelte`
3. Use SDK functions for API calls
4. Use semantic color tokens for styling
When backend endpoints change, update the backend and restart it, fetch the new OpenAPI spec (the docs workflow does this automatically), run `npm run generate:api`, and fix any TypeScript errors from changed types. To add new routes, create a component in `src/routes/`, add a `<Route>` entry in `App.svelte`, use SDK functions for API calls, and use semantic color tokens for styling.

## Production build

The production build runs `npm run build`, which:

1. Compiles TypeScript with source maps
2. Processes Svelte components in production mode (no dev warnings)
3. Extracts and minifies CSS
4. Splits code into chunks
5. Minifies JavaScript with Terser (removes console.log)
6. Outputs to `public/build/`

The Docker build copies `public/` to nginx, which serves static files and proxies `/api/` to the backend.
The production build runs `npm run build`, which compiles TypeScript with source maps, processes Svelte components in production mode without dev warnings, extracts and minifies CSS, splits code into chunks, minifies JavaScript with Terser removing console.log calls, and outputs everything to `public/build/`. The Docker build copies `public/` to nginx, which serves static files and proxies `/api/` to the backend.

## Troubleshooting

### TypeScript errors after SDK regeneration

If the backend changed response types, update components to match. The SDK provides exact types — check `types.gen.ts` for the new structure.

### Styles not applying

Ensure the class exists in Tailwind's default utilities or is defined in `app.css`. Check for typos in semantic token names (e.g., `bg-default` vs `bg-bg-default`).

### Dev server certificate errors
If you see TypeScript errors after SDK regeneration, check `types.gen.ts` for the new structure and update components to match the changed response types.

The dev server requires certificates at `./certs/server.key` and `./certs/server.crt`, and the CA at `/shared_ca/mkcert-ca.pem`. Run the cert-generator container first via Docker Compose.
For styles not applying, ensure the class exists in Tailwind's default utilities or is defined in `app.css`, and check for typos in semantic token names (e.g., `bg-default` vs `bg-bg-default`).

### API calls failing in development
Dev server certificate errors mean the certificates at `./certs/server.key` and `./certs/server.crt` are missing, or the CA at `/shared_ca/mkcert-ca.pem` isn't available — run the cert-generator container first via Docker Compose.

Verify the backend is running and healthy. The dev server proxies to `https://backend:443` check Docker networking if the container can't resolve the hostname.
If API calls fail in development, verify the backend is running and healthy; the dev server proxies to `https://backend:443`, so check Docker networking if the container can't resolve the hostname.
Loading
Loading