Plugins
Davaux plugins let you extend the framework in four ways: adding esbuild transforms to the build pipeline, registering new route file suffixes so the scanner recognises them, shipping reactive components from a package, and shipping island components from a package. All four are exposed through a single DavauxPlugin interface.
Using plugins
Pass plugins to davaux.config.ts:
// davaux.config.ts
import { defineConfig } from 'davaux/config'
import { markdown } from '@davaux/markdown'
import { mdx } from '@davaux/mdx'
export default defineConfig({
plugins: [markdown(), mdx()],
})
Plugin esbuild contributions are added to every build context — server routes, the islands client bundle, and the optional user client bundle. Scanner suffix entries extend which files the route scanner picks up.
Official plugins
| Package | Route suffix | Description |
|---|---|---|
@davaux/markdown | .page.md | Markdown routes with YAML frontmatter |
@davaux/mdx | .page.mdx | MDX routes — markdown with JSX component imports |
Authoring a plugin
A plugin is a factory function that returns a DavauxPlugin object:
import type { DavauxPlugin } from 'davaux/config'
export function myPlugin(): DavauxPlugin {
return {
name: 'my-plugin',
esbuild: [myEsbuildPlugin()],
scanner: {
suffixes: [['.page.myext', 'page']],
},
}
}
DavauxPlugin fields:
| Field | Type | Description |
|---|---|---|
name | string | Unique identifier, used in error messages |
esbuild | Plugin[] | esbuild plugins added to all build contexts |
scanner.suffixes | [string, RouteType][] | File suffix + route type pairs for the scanner |
srcDirs | string[] | Absolute paths to source directories scanned for reactive components (auto-detected via the @jsxImportSource davaux/client pragma) |
islandsDir | string | Absolute path to a directory of island components — all files included unconditionally |
RouteType is 'page' | 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options'.
Shipping reactive components from a plugin
If your plugin includes reactive components that should run on the client, set srcDirs to an array of absolute paths to the directories that contain them. Davaux scans each directory and automatically includes any file with the @jsxImportSource davaux/client pragma in the islands bundle — no extra configuration is required from the app author beyond enabling the plugin.
import { resolve } from 'node:path'
import type { DavauxPlugin } from 'davaux/config'
export function myPlugin(): DavauxPlugin {
return {
name: 'my-plugin',
srcDirs: [resolve(import.meta.dirname, 'components')],
}
}
This is the right approach when your plugin ships reactive components alongside server components in the same directory — for example a dist/components/ folder that contains both server-renderable and client-reactive files. Only files with the @jsxImportSource davaux/client pragma are picked up; purely server-side files in the same directory are left out.
Components in the declared directories follow the standard reactive component rules — use the /** @jsxImportSource davaux/client */ pragma, export with reactive(), and keep props JSON-serialisable:
/** @jsxImportSource davaux/client */
import { createSignal } from 'davaux/client'
import { reactive } from 'davaux'
function Counter({ initial = 0 }: { initial?: number }) {
const [count, setCount] = createSignal(initial)
return <button onClick={() => setCount(c => c + 1)}>{() => count()}</button>
}
export default reactive(Counter)
App authors import and use them exactly like any local reactive component:
import Counter from '@my-org/my-plugin/components/Counter'
export default definePage(() => (
<main>
<Counter initial={5} />
</main>
))
See Islands for the full reactive component authoring guide.
Shipping islands from a plugin
If your plugin includes interactive client-side components, set islandsDir to the absolute path of the directory containing them. Davaux scans that directory alongside the app's own src/islands/ and merges the results into a single client bundle — no extra configuration is required from the app author beyond enabling the plugin.
import { resolve } from 'node:path'
import type { DavauxPlugin } from 'davaux/config'
export function myPlugin(): DavauxPlugin {
return {
name: 'my-plugin',
islandsDir: resolve(import.meta.dirname, 'islands'),
}
}
Components inside the declared directory follow the same rules as reactive components — use the /** @jsxImportSource davaux/client */ pragma, export with reactive(), and keep props JSON-serialisable. App authors import and use them exactly like any other reactive component:
import SomeWidget from '@my-org/my-plugin/islands/SomeWidget.tsx'
export default definePage(() => (
<main>
<SomeWidget label="hello" />
</main>
))
See Islands for the full island authoring guide.
Example: a YAML front-matter page plugin
This example adds .page.yaml as a route type that renders a simple data dump page — a minimal but complete plugin:
import type { Plugin } from 'esbuild'
import type { DavauxPlugin } from 'davaux/config'
import { readFileSync } from 'node:fs'
function yamlPagePlugin(): Plugin {
return {
name: 'davaux-yaml-page',
setup(build) {
build.onLoad({ filter: /\.page\.yaml$/ }, (args) => {
const raw = readFileSync(args.path, 'utf-8')
// Real implementation would parse YAML; this serialises the raw string
const contents = `
import { definePage } from 'davaux'
export default definePage((ctx) => {
ctx.head.title = 'YAML Data'
return \`<pre>\${${JSON.stringify(raw)}}</pre>\`
})
`
return { contents, loader: 'ts' }
})
},
}
}
export function yaml(): DavauxPlugin {
return {
name: 'davaux-yaml',
esbuild: [yamlPagePlugin()],
scanner: { suffixes: [['.page.yaml', 'page']] },
}
}
esbuild plugin context
Davaux runs three separate esbuild build contexts:
- Server routes — all files under
src/routes/; island imports are auto-wrapped server-side - Islands client bundle — app islands from
src/islands/plus any directories declared via pluginislandsDir, plus reactive components auto-detected from pluginsrcDirs, all recompiled with the client JSX runtime - User client bundle —
src/client.tsif present
Your plugin's esbuild array is added to all three contexts. If your transform only makes sense in one context (e.g. a server-only loader), check the build options or use a filter that matches only the relevant files.
Scanner suffixes
The scanner looks for files matching known suffixes to build the route manifest. Without registering a suffix, files with your custom extension are silently ignored even if they export the right define* wrappers.
The suffix registration format is [suffix, routeType]:
scanner: {
suffixes: [
['.page.md', 'page'], // GET page route
['.api.ts', 'get'], // explicit GET API route
]
}
Route type controls which HTTP methods the route responds to — 'page' responds to GET and HEAD, 'get' responds to GET only, and so on.