Plugin System - Krate Docs
You're viewing an older version ().
Krate Docs

Extend Krate with Go hooks or JavaScript/TypeScript plugins.

#Plugin System

Krate's plugin system is a single, unified interface with 9 hooks across two runtimes. Built-in plugins are written in Go; community plugins are either JavaScript modules executed inside the embedded QuickJS runtime — no subprocess, no stdin/stdout protocol — or Go plugins run as a subprocess over HashiCorp go-plugin.

#Community plugin protocol

Community JS plugins are JavaScript or TypeScript modules executed inside the embedded QuickJS runtime (modernc.org/quickjs). The module is bundled with esbuild (which transpiles .ts/.tsx) into a self-contained IIFE and its hooks are called directly from Go. Plugin modules may be plain .js, .mjs, .cjs or TypeScript .ts/.tsx; a plugin directory resolves its entry point in the same order (index.js, index.ts, plugin.tsx, ...).

Plugins can be authored with full type-safety via the @krate/plugin package, which ships typed hook contexts, outputs, and descriptors plus compile-time-only helpers (definePlugin, definePluginHooks, defineGoPlugin). The helpers are erased when the module is bundled, so they add no runtime cost inside QuickJS.

// plugins/my-plugin/index.ts
import { definePlugin, definePluginHooks } from "@krate/plugin";
import type { Krate, PluginOutput } from "@krate/plugin";

interface MyPluginOptions {
  greeting?: string;
}

export const hooks = definePluginHooks<MyPluginOptions>({
  BeforeBuild(ctx, options, krate: Krate): PluginOutput {
    krate.log("building with greeting", options.greeting);
    return { files: [{ path: "note.txt", content: "hi" }] };
  },
  AfterRender(ctx, options, krate: Krate): PluginOutput {
    return {
      html: "<b>" + ctx.html,
      headHTML: "<meta ...>",
      rawCSS: ".x{}",
    };
  },
});

export default function myPlugin(options: MyPluginOptions = {}) {
  return definePlugin({
    name: "my-plugin",
    order: 20,
    module: typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : "",
    options,
  });
}
  • Hook signature — every hook receives (ctx, options, krate) where ctx is the JSON-serialized hook context (lowercase fields like ctx.html, ctx.page, ctx.outName, ctx.headHTML, ctx.rawCSS) and krate is the richer build context: { root, projectRoot, outDir, pagesDir, config, dev, devMode, pages, version }. The @krate/plugin types name these BuildContext, ParseContext, MarkdownContext, RenderContext, PageContext, BuildResultContext, ServeRequestContext, and ServeResponseContext.
  • krate capabilities — instead of raw fs guesswork or a hand-rolled node_modules walk, use the host-backed methods: krate.resolveFile(spec) and krate.readFile(spec) (root-anchored; bare specifiers resolve through node_modules), krate.emitFile(path, content) (write into the output directory), krate.writeFileToRoot(rel, content) (static assets under the project root), and krate.injectHead(html) / krate.injectCSS(css). Every path is anchored to the project root and traversal outside it is rejected.
  • Logging — krate.warn(...) always writes a prefixed warning to stderr; krate.log(...) is diagnostic output shown only when the build runs with --verbose (which also prints a per-hook trace: plugin, hook, and duration). Plain console.* output is prefixed with [plugin:<name>] so it is attributable.
  • Typed options — pass your plugin's options type to definePluginHooks<MyOptions>({ ... }) and every hook's options argument is typed instead of unknown:
interface MyOptions { greeting?: string }
  export const hooks = definePluginHooks<MyOptions>({
    BeforeBuild(ctx, options) { return { html: options.greeting ?? "" }; },
  });
  • Return value — hooks return { files, routes, generatedPages, html, headHTML, rawCSS, scripts, metaTags, ast } (all optional; may be a Promise; PluginOutput in @krate/plugin). files are written into the output directory (path traversal is rejected), routes become static HTML pages, generatedPages feed the page pipeline, and html/headHTML/rawCSS/scripts/metaTags mutate the hook context. At AfterParse, ctx.program is the kind-tagged AST document; mutate it and return it as ast to rewrite the tree (same capability as Go plugins). Use ASTTypes / isAstKind from @krate/plugin for type-safe kind checks instead of raw strings.
  • Runtime capabilities — bundled plugins can use import fs from 'fs' / import path from 'path' (polyfilled) plus Web API polyfills (fetch, URL, Headers, Response, TextEncoder, timers, process.env). Non-relative third-party imports are left external and unavailable.

#Go plugins

Community plugins can also be Go programs run as a subprocess via HashiCorp go-plugin. They use the public SDK (github.com/kratejs/krate/packages/compiler/pluginsdk, package plug) and are distributed as an npm package whose descriptor reports runtime: 'go' and per-platform binary paths:

module.exports = function () {
  return {
    name: "my-plugin",
    module: (typeof import.meta !== 'undefined' && import.meta.url) ? import.meta.url : '',
    runtime: "go",
    binaries: { "linux-amd64": "bin/my-plugin-linux-amd64" },
  };
};

module points at the descriptor module itself so the binaries paths resolve against the package root.

Go plugins are trusted dependencies and run with full subprocess access. They can edit the parsed AST (ctx.Program is a live *ast.Program), and their serve hooks handle request-time transforms.

#Serve hooks

Two additional request-time hooks run in the dev server: ServeRequest (mutate or short-circuit an incoming request before it is served) and ServeResponse (rewrite a non-streaming response after it has been buffered). Streaming responses pass through unchanged.

#Config usage (typed)

import { defineConfig, sitemap, docs } from '@krate/core';
import demoPlugin from './plugins/krate-plugin-demo';

export default defineConfig({
  plugins: [
    sitemap({ baseUrl: "https://example.com" }),
    docs({ contentDir: "src/content/docs", title: "Docs" }),
    demoPlugin({ greeting: "Hello!" }),
  ],
});

Each factory returns a serializable descriptor: { name, order, options } (built-ins), { name, order, module, options } (JS community plugins), or { name, order, module, runtime: "go", hooks, binaries } (Go plugins). The @krate/plugin helpers type each shape: definePlugin for JS community plugins, defineGoPlugin for Go-plugin descriptors.

#Built-in plugins

PluginPurpose
sitemapGenerates sitemap.xml
icons<Icon> → Iconify SVG with disk cache
imageprocessing<Image> → responsive <picture> with srcset
markdownMarkdown/MDX compilation
cspContent Security Policy meta tag
docsDocumentation site generator with WASM search

See Guides: Creating Plugins for an authoring walkthrough (JS and Go), Guides: Customizing Docs, and the demo plugins in the examples (examples/plugins/).

Last updated: 2026-10-16

Contributors: Noinkin