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

vesk.config.ts
									
										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.

my-plugin.ts
									
										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.