docs / Language
Configuration
vesk.config.js / vesk.config.ts, defineConfig and how it normalizes security, environment loading, platforms, and the app-level events + context system.
Vesk projects are configured with `vesk.config.js` or `vesk.config.ts` in the project root. TypeScript configs are transpiled inline at startup — no separate build step needed.
Basic config
import { defineConfig } from '@vesk/compiler'
export default defineConfig({
appDir: './app',
outDir: '.vesk',
publicDir: './public',
security: 'strict',
md: {
html: 'escape',
},
})
Loading
- The CLI looks for `vesk.config.js` first, then `vesk.config.ts`; if neither exists, the app runs with defaults.
- TS configs are transpiled inline (`typescript.transpile`); `@vesk/compiler` imports are stripped, `defineConfig` / `definePlugin` / `preset` are injected via `globalThis.__vesk_inject`, the result is written to `.vesk/config.tmp.js`, imported, and the exports map makes the helpers available inside the config.
- The loaded object flows through `defineConfig` (normalization + security defaults) and then `validateConfig` (plugin contract checks) before any command runs.
- Environment files load before the config (`loadEnvFiles` runs first), so `process.env` is already populated when `vesk.config.ts` evaluates.
Config options
| Option | Type | Description |
|---|---|---|
| appDir | string | Source directory holding routes and components. CLI default: `./app`. |
| outDir | string | Build output directory. Default: `resolve(appDir, '..', '.vesk')` — the `.vesk/` folder. |
| publicDir | string | Static-assets directory served as-is. CLI default: `./public`. |
| ssg | SSGConfig | Static-site generation marker: `{ getStaticPaths?() }`. Pages exporting `getStaticProps` (dynamic pages exporting `getStaticPaths`) pre-render into `.vesk/prerendered/`. |
| plugins | VeskPlugin[] | Plugin objects registered into the build/dev pipelines (see the Plugin API page). |
| security | SecurityConfig | Preset name, object, `false`, or a function receiving `preset` — see Security below. |
| routeDataCache | number | Client-router route-data freshness TTL in ms. Default `0` = always fetch fresh server data on an SPA visit. |
| md | MdConfig | Global markdown options for `<Md>`: `html` (`'escape'` default | `'allow'` | `'allowlist'`) and `allowTags`. `'allowlist'` with no `allowTags` uses the built-in default tag list (a, abbr, b, br, code, em, i, kbd, mark, q, s, samp, small, span, strong, sub, sup, time, u, var, wbr, and more). |
defineConfig
`defineConfig(config)` returns the config after normalizing it. It resolves `security` (preset string to object, `'off'`/`false` to `{}`, function to its return value) and then fills in guard defaults that apply on top of any preset.
- `security` may be a named preset (`'strict'`, `'default'`, `'minimal'`), `'off'` / `false`, a plain object, or a function `(preset) => VeskSecurity` that receives the `preset()` helper.
- Applying the defaults fills: `autoEscape: true`, `csrf: true` (same-origin check for unsafe methods), `xFrameOptions: 'DENY'`, the default Content-Security-Policy, and `redactLogs: true` — even when `security` was `false`/`'off'` (those only suppress the named-preset picks).
- The default CSP is `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'`.
- `routeDataCache` defaults to `0` when absent.
- `md.html` must be one of `escape` / `allow` / `allowlist` (anything else throws a config error); `md.allowTags` entries are lowercased and stripped to `[a-z0-9-]`.
Security presets
The named presets are built in `@vesk/compiler/src/config.ts`. `'default'` is defined as a shallow copy of `'strict'`. Presets only set the fields shown; the rate-limit / CORS / trust-proxy knobs live on `VeskSecurity` but are not part of any preset.
| Preset | Auto-escape | CSRF | X-Frame-Options | HSTS | Content-Security-Policy | Referrer-Policy | Redact logs |
|---|---|---|---|---|---|---|---|
| 'strict' | ✓ | ✓ | DENY | max-age=31536000; includeSubDomains | default CSP (lockdown) | strict-origin-when-cross-origin | ✓ |
| 'default' | ✓ | ✓ | DENY | max-age=31536000; includeSubDomains | default CSP | strict-origin-when-cross-origin | ✓ |
| 'minimal' | ✓ | — | SAMEORIGIN | — | — | — | — |
`security: false` / `'off'` sets the security object to `{}`. `defineConfig` then still applies its guard defaults (auto-escape, same-origin CSRF, `X-Frame-Options: DENY`, the default CSP, log redaction). `securityHeaders` always emits `X-Content-Type-Options: nosniff` and `X-XSS-Protection: 0` as well.
preset()
`preset(name, overrides?)` returns a security object for `'production'` (strict settings) or `'development'` (strict minus Content-Security-Policy). Unknown names throw, and `overrides` are merged last. It is normally reached through the `security` function form:
import { defineConfig } from '@vesk/compiler'
export default defineConfig({
security: (preset) =>
preset('production', {
contentSecurityPolicy: false,
}),
})
Environment variables
- `vesk` loads `.env` then `.env.local` from the project root before anything else; because keys are only set when not already in `process.env`, `.env.local` wins over `.env`.
- Only `KEY=VAL` lines are read. Blank lines and `#` comments are skipped; surrounding quotes on values are stripped.
- Keys already present in `process.env` are never overridden by either file.
- Loaded before `vesk.config.ts` evaluates, so config files can read `process.env` at load time.
There is no built-in mechanism that copies environment variables into client code. Anything the client needs must be fetched or explicitly wired — do not rely on env vars leaking into the client bundle.
Platforms
`vesk build --platform <name>` picks the deployment artifact. Without the flag, the adapter auto-detects the platform from well-known CI/build environment variables and otherwise defaults to a standard Node server.
| Platform | How it is selected |
|---|---|
| node | Default when nothing else matches. |
| vercel | `--platform vercel`, or `VERCEL` / `VERCEL_ENV` / `NOW_REGION` / `VERCEL_GIT_COMMIT_SHA` are set. |
| netlify | `--platform netlify`, or `NETLIFY` / `NETLIFY_BUILD_CONTEXT` / `NETLIFY_LOCAL` / `NETLIFY_EDGE` are set. |
| cloudflare | `--platform cloudflare`, or `CF_PAGES` / `CF_PAGES_BRANCH` / `CF_PAGES_URL` / `CLOUDFLARE_WORKERS` / `WORKERS_NAME` are set. |
| deno | `--platform deno`, or `DENO_DEPLOYMENT_ID` / `DENO_REGION` / `DENO_DEPLOY_URL` are set. |
| aws | `--platform aws`, or `AWS_LAMBDA_FUNCTION_NAME` / `AWS_LAMBDA_FUNCTION_VERSION` / `LAMBDA_TASK_ROOT` / `LAMBDA_RUNTIME_DIR` are set. |
| edge | Edge deployment via `--platform edge` (an artifact builder); `--target edge` separately selects the edge runtime target. |
| coxmos | `--platform coxmos`, or `COXMOS` / `COXMOS_DEPLOYMENT_ID` / `COXMOS_ENV` / `VESK_DEPLOY` / `VESK_PLATFORM=coxmos` are set. |
App events and server context
App-level lifecycle events live in one convention file: `app/_events.ts` (or `app/_events.js`), which exports optional `onStart`, `onRequest` and `onStop` handlers. The `_` prefix marks the file private — it is never routed. Both the events file and plugin `onStart`/`onStop` hooks receive a `ServerEventContext`.
import type { ServerEventContext } from '@vesk/types'
export async function onStart(ctx: ServerEventContext) {
ctx.set('bootedAt', Date.now())
}
export async function onRequest(ctx: ServerEventContext) {
const hits = (ctx.get('hits') as number) || 0
ctx.set('hits', hits + 1)
}
- `onStart` runs once when a dev/start server boots, before it begins listening; on serverless/edge targets it runs lazily once per isolate. Node servers additionally expose `server`, `port`, and `host`.
- `onRequest` runs for every request and receives the per-request `request`, `params`, `url`, `cookies` and `locals` in addition to the shared store.
- `onStop` runs on graceful shutdown (`SIGINT`/`SIGTERM`). Node dev/start servers only — serverless/edge targets have no shutdown lifecycle.
- `ctx.set(key, value)` / `ctx.get(key)` read and write the process-wide store exposed as `serverLocals`. Values set during `onStart` are pre-seeded into every request's `locals()`.
- Dev servers run the live handlers and reload them on HMR; production builds bake `app/_events.ts` into `server/events.js` at build time.
| Field | Description |
|---|---|
| server | The running Node `http.Server`, or `null` on serverless/edge targets and at build time. |
| port / host | The port (0 without a persistent server) and bind address. |
| request / params / url / cookies | Per-request values, populated for `onRequest`. |
| locals | Per-request context, pre-seeded from the server-wide store. |
| serverLocals | The process/isolate-wide store shared across every request. |
| set(key, value) / get(key) | Write / read the server-wide store. |
The plugin `onStart` and `onStop` hooks receive the same `ServerEventContext` — see the Plugin API page. Plugin objects only exist in dev/build processes; the production process runs the baked `app/_events.ts` handlers instead.
Component context (createContext)
`createContext` is a component-scoped runtime API (from `@vesk/runtime/src/context.ts`), not a config or plugin feature. It is auto-imported from `@vesk/runtime` whenever a `.vsk` component uses it. `createContext(defaultValue)` returns a `Context<T>`: `get()` walks up the active-component chain returning the nearest value set by an ancestor (falling back to the default), and `set(value)` stores a value for the currently rendering component (it throws `No active component found, cannot set context` outside a render). `getActiveComponent()` / `setActiveComponent()` are the runtime's lower-level hooks for tracking the active component and are used by renderer internals.
component Theme {
const Theme = createContext('light')
Theme.set('dark')
<p>{Theme.get()}</p>
}
Component context (`createContext` / `Context`) is per-component-tree state and is unrelated to the server-wide `serverLocals` store above, and to the plugin `provides` injection mechanism.