Codegen Plugins (Node)
Node plugins let you hook into Houdini’s codegen pipeline from JavaScript or TypeScript. They follow the same lifecycle as Go plugins but are written as plain Node.js scripts and distributed as normal npm packages.
Defining a Plugin
A Node plugin is a script that calls plugin() from houdini/node. Pass it a name, an order, and an object of hook handlers:
import { plugin } from 'houdini/node'
plugin({ name: 'my-plugin', order: 'after', hooks: { async validate(ctx, payload) { // inspect documents and throw PluginError to surface problems }, async generateRuntime(ctx, payload) { // write project-wide output files }, },})plugin() handles all the transport plumbing (registering with the orchestrator, receiving hook invocations, and sending results back) so the script itself stays minimal.
Plugin Order
'before': runs beforehoudini-core'core': reserved forhoudini-coreand framework plugins'after': runs afterhoudini-core(the right choice for most plugins)
Entry Point
The script is the entry point. Wire it up in package.json’s bin field so the orchestrator can launch it:
{ "name": "my-plugin", "bin": "dist/index.js"}Hook Handlers
Each hook is an async function that receives a PluginContext and a payload, and can return a result object:
type HookHandler = ( ctx: PluginContext, payload: Record<string, any>) => Promise<Record<string, any> | undefined> | Record<string, any> | undefinedThe available hooks mirror the Go pipeline exactly:
| Hook | When it fires |
|---|---|
config | Plugin is loading; return default config values |
afterLoad | All plugins have loaded |
schema | Add custom directives or types to the project schema |
extractDocuments | Parse source files; receives { filepaths: string[] } |
afterExtract | All documents extracted |
beforeValidate | Transform documents before validation |
validate | Report validation errors |
afterValidate | Documents are valid; last chance to transform before codegen |
beforeGenerate | Runs just before artifact generation |
generateDocuments | Write per-document output files |
generateRuntime | Write project-wide runtime files |
afterGenerate | Runs after all generation is complete |
Errors
Throw PluginError from any hook to surface a structured error to the user:
import { plugin, PluginError } from 'houdini/node'
plugin({ name: 'my-plugin', order: 'after', hooks: { async validate(ctx, payload) { throw new PluginError({ message: 'document must have a name', kind: 'validation', locations: [{ filepath: 'src/routes/+page.svelte', line: 12 }], }) }, },})Plain Error throws are also caught and forwarded, but without file locations or a kind.
Plugin Context
Every hook receives a PluginContext as its first argument:
type PluginContext = { taskId: string pluginDirectory: string db: Db invokeHook( hook: string, payload?: Record<string, any>, options?: { parallel?: boolean } ): Promise<Record<string, any>>}pluginDirectory: absolute path to the directory containing your plugin’s entry point. Use it to resolve bundled assets.db: a connection to the shared SQLite database. Exposesget,all, andrunfor reading and writing pipeline data directly:
async validate(ctx, payload) { const docs = ctx.db.all(`SELECT name, filepath FROM documents WHERE kind = 'query'`) for (const doc of docs) { if (!doc.name.endsWith('Query')) { throw new PluginError({ message: `query name must end with 'Query'`, kind: 'validation', locations: [{ filepath: doc.filepath }], }) } }},invokeHook: call another pipeline hook from within your handler. Results are keyed by plugin name.
Optional Fields
The plugin config accepts a few additional fields for plugins that need to ship runtime code:
includeRuntime: a path (relative to your plugin entry point) to a directory that Houdini will copy into the project’s generated runtimestaticRuntime: a path (relative to your plugin entry point) to a directory whose contents are copied before codegen runs (see below)configModule: a path to a JavaScript module that exports config values to merge into the project configclientPlugins: an object of client-side plugins to inject into the user’sHoudiniClient
plugin({ name: 'my-plugin', order: 'after', includeRuntime: './runtime', clientPlugins: { 'my-plugin/runtime/clientPlugin': null, }, hooks: { /* ... */ },})Static Runtimes
staticRuntime points to a directory (relative to your plugin entry point) whose contents are copied into the project during the afterLoad phase, before document discovery and codegen run.
plugin({ name: 'my-plugin', order: 'after', staticRuntime: './static', hooks: { /* ... */ },})Because the copy happens before document discovery, any .graphql files in that directory are treated as project documents. That makes staticRuntime the right place to ship GraphQL fragments or queries that users can reference directly in their own operations:
fragment UserFields on User { id name email}A user can then spread ...UserFields in their own queries without defining the fragment themselves. The plugin owns the definition; the project just consumes it.
The main distinction from includeRuntime is timing: includeRuntime is copied during the generateRuntime phase (after documents are already collected), so its .graphql files arrive too late to be discovered. Use staticRuntime whenever the content needs to participate in the document graph, and includeRuntime for TypeScript runtime code that doesn’t.
Vite Integration
If your plugin needs to add transforms to the user's Vite build, export a /vite sub-module from your npm package. Houdini picks it up automatically and includes it in the project's Vite config, with no manual wiring required on the user's end.
The sub-module just needs to export a default function that returns a Vite plugin:
import type { Plugin } from 'vite'
export default function myPlugin(): Plugin { return { name: 'my-plugin', transform(code, id) { // ... }, }}Wire it up in package.json's exports field:
{ "exports": { ".": "./dist/index.js", "./vite": "./dist/vite.js" }}Any project that installs your plugin will get the Vite integration automatically.