DAVAUX
Alpha

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

PackageRoute suffixDescription
@davaux/markdown.page.mdMarkdown routes with YAML frontmatter
@davaux/mdx.page.mdxMDX 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:

FieldTypeDescription
namestringUnique identifier, used in error messages
esbuildPlugin[]esbuild plugins added to all build contexts
scanner.suffixes[string, RouteType][]File suffix + route type pairs for the scanner
srcDirsstring[]Absolute paths to source directories scanned for reactive components (auto-detected via the @jsxImportSource davaux/client pragma)
islandsDirstringAbsolute 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:

  1. Server routes — all files under src/routes/; island imports are auto-wrapped server-side
  2. Islands client bundle — app islands from src/islands/ plus any directories declared via plugin islandsDir, plus reactive components auto-detected from plugin srcDirs, all recompiled with the client JSX runtime
  3. User client bundlesrc/client.ts if 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.