docs / Tooling
Plugin API
The VeskPlugin shape, definePlugin, every real hook, provides, registration and validation, and how @vesk/plugin-tailwind integrates.
Vesk plugins extend the build pipeline and dev server. There is no Vite adapter — `vesk dev` / `vesk build` are the entry points, and plugins are plain objects registered in `vesk.config.ts`. The hook contract is defined by `VeskPlugin` in `@vesk/types`.
Plugin shape
- `name: string` — required; used for activation state and the validation error message.
- `provides` — a record of values or factories injected into the per-request middleware context (see below).
- The hooks below — every one is optional, but a plugin with no recognized hook and no `provides` is rejected.
Registering plugins
import { defineConfig, definePlugin } from '@vesk/compiler'
import tailwindcss from '@vesk/plugin-tailwind'
export default defineConfig({
plugins: [
tailwindcss({ entry: 'src/global.css', appDir: 'app' }),
definePlugin({
name: 'my-plugin',
onBuildEnd: async () => {
console.log('build finished')
},
}),
],
})
Hooks
| Hook | Signature | When it runs |
|---|---|---|
| onBuildStart | () => void | Promise<void> | Start of `vesk build` (also once at dev-server CSS pipeline boot). |
| onBuildEnd | () => void | Promise<void> | End of `vesk build`. |
| onCSS | (content: string, filePath: string) => string | null | Transform CSS output in dev and build. Return `null` to keep the input unchanged. |
| onTransformJS | (code: string, filePath: string) => string | null | Transform an emitted client bundle or code-split chunk source. |
| onFileWatch | (filePath: string) => { handled: boolean } | A recognized hook where a plugin signals whether it claims a file-change event it manages itself. |
| onRequest | (ctx: MiddlewareContext) => void | Promise<void> | Every request passing through the middleware chain, after `provides` are injected. |
| onStart | (ctx: ServerEventContext) => void | Promise<void> | Once when a dev/start server boots (once per isolate on serverless/edge targets), before it begins listening. |
| onStop | (ctx: ServerEventContext) => void | Promise<void> | Graceful shutdown (SIGINT/SIGTERM) on Node dev/start servers; no-op on serverless/edge. |
| onHead | (headHtml: string, ctx?: RenderPluginContext) => string | null | Rewrite the assembled `<head>`; runs in config order on every rendered document. |
| onHtml | (html: string, ctx?: RenderPluginContext) => string | null | Rewrite the finalized full HTML document. Non-streamed documents only. |
Hook contracts
- `onCSS`, `onTransformJS`, `onHead`, `onHtml` return the new string to replace output, or `null` to leave it unchanged; a string return always replaces.
- `onHead` hooks chain in config order — a later hook sees the output of the earlier hooks. In dev and build/SSG the live hooks run; in production the build bakes the plugin head contribution into `headExtra` (merged into SSR functions and not-found/error pages, with the page head winning).
- `onHtml` runs only for non-streamed documents (SSG pre-render and buffered `renderFullPage`, including not-found/error). Streaming paths and the bake-for-prod path apply `onHead` only.
- `onStart` / `onStop` receive a `ServerEventContext` (same shape the app-level `app/_events.ts` handlers get — see the config page). Values `set()` in `onStart` land in the server-wide store and are pre-seeded into every request's `locals()`.
- `onRequest` receives a `MiddlewareContext` (per-request `locals`, `params`, `cookies`, `request`, `set`/`get`) and runs inside the middleware chain.
- Plugin objects exist only in dev/build processes — the production server runs baked assets, so prod-time behavior must be expressed through bake-time hooks (`onHead` → `headExtra`, `onBuildEnd`, static output).
Registration and validation
- `definePlugin(plugin)` requires an object with a non-empty `name` string and throws otherwise.
- After config load the CLI runs `validateConfig`, which rejects a plugin with no `name` and any plugin whose only surface is unrecognized — it must implement at least one of `onCSS`, `onFileWatch`, `onTransformJS`, `onBuildStart`, `onBuildEnd`, `onRequest`, `onStart`, `onStop`, `onHead`, `onHtml`, or provide a non-empty `provides`.
- Config-declared plugins default to active. Activation can be overridden through the dev panel / `.vesk/plugins.json` state file; an inactive plugin is dropped from every build and dev pipeline, and an uninstalled plugin can never be active.
provides
`provides` is a `Record<string, value-or-factory>`. When a request enters the middleware chain, each entry is resolved — a function is invoked (and awaited when async), any other value is used as-is — and written into the per-request context with `ctx.set(key, …)` before `onRequest` runs.
import { definePlugin } from '@vesk/compiler'
export default definePlugin({
name: 'my-plugin',
provides: {
greeting: () => 'hello from the plugin',
},
onRequest: async (ctx) => {
const g = ctx.get('greeting') ?? ''
ctx.set('greetingLength', g.length)
},
})
@vesk/plugin-tailwind
The Tailwind integration ships as a plugin factory imported by default export. `tailwindcss(options?)` accepts `{ entry?, appDir? }` with defaults `entry: 'src/global.css'` and `appDir: 'app'`, and returns a plugin named `@vesk/plugin-tailwind`.
- `onCSS` fires only for the configured entry file — every other `filePath` passes through untouched.
- It extracts Tailwind v4 directives (`@import tailwindcss`, `@source`, `@theme`, `@layer base|components|utilities`, `@utility`) from the entry, compiles the remaining user CSS with Tailwind, and returns the compiled stylesheet.
- Class purging: it scans `.vsk`, `.js`, `.ts`, `.jsx`, `.tsx` files for `class="…"` / `classname="…"` attributes and feeds those class strings (length > 1, not `{…}` expressions) into the Tailwind candidate list.
- On a compile error it logs the failure and falls back to the non-directive user CSS.
- Dev: the compiled CSS is rebuilt live and served at `/_vesk/static/global.css`; editing a `.vsk` or the entry CSS re-runs `onCSS` and hot-swaps the stylesheet.
- Build: the output is written to `.vesk/static/global.css`, which the rendered HTML links as `/_vesk/static/global.css`.
- `onBuildStart` resets its dependency set, and `onFileWatch` reports the dependency files it tracks — the plugin keeps a `dependencies` set fed by Tailwind's `onDependency` callbacks.
Because the export is the default, configure it with `import tailwindcss from '@vesk/plugin-tailwind'`. A bare CSS pipeline (no plugin) still copies `src/global.css` to `.vesk/static/global.css` — the plugin only decides whether that copy is Tailwind-compiled.
For app-level lifecycle events (`app/_events.ts`, `ServerEventContext`, `serverLocals`) see the Configuration page's App events section — plugin `onStart`/`onStop` share that same context type.