# Plugins (/docs/code/codegen/plugins)

---
title: Plugins
description: A codegen plugin teaches the generator a new file convention, a new builder method or a whole new generated file. This is the contract, field by field.
kind: reference
package: questpie
---

## The shape

```ts
import type { CodegenPlugin } from "questpie/codegen";

export function openApiPlugin(): CodegenPlugin {
	return {
		name: "questpie-openapi",
		targets: {
			server: {
				root: ".",
				outputFile: "index.ts",
				discover: {
					openapi: { pattern: "config/openapi.ts", configKey: "openapi" },
				},
			},
		},
	};
}
```

| Field        | Type                                        | What it is                          |
| ------------ | ------------------------------------------- | ----------------------------------- |
| `name`       | `string`                                    | Unique. Plugins are deduped by it.  |
| `targets`    | `Record<string, CodegenTargetContribution>` | What this plugin adds, per target.  |
| `validators` | `CrossTargetValidator[]`                    | Checks that run after every target. |

A target is one output directory. `server` writes `index.ts`, `app-factory.ts`,
`factories.ts` and the type layers beside them. `admin-client` writes
`client.ts`. A plugin can invent its own target id.

The core plugin is prepended on every run. It declares the built-in categories
and scaffolds. You never register it.

## Registering one

Put it on the config, or ship it on a module.

```ts title="questpie.config.ts"
export default runtimeConfig({
	plugins: [openApiPlugin()],
});
```

```ts title="the module a package ships"
export const myModule = module({
	name: "acme-thing",
	plugin: myPlugin(),
});
```

The module route is the one to prefer. Codegen walks `modules.ts` before
discovery, collects every `plugin` it finds, and the user adds nothing. The walk
is depth first, submodules before the module itself. Duplicate names are dropped
and the first one wins. A plugin registered on the config beats a module one of
the same name.

## What a target contribution holds

`root` and `outputFile` are required. Everything else is optional.

| Field            | Type                                      | What it adds                                         |
| ---------------- | ----------------------------------------- | ---------------------------------------------------- |
| `root`           | `string`                                  | Discovery root, relative to the server root          |
| `outDir`         | `string`                                  | Output directory inside `root`, default `.generated` |
| `outputFile`     | `string`                                  | The main generated file                              |
| `moduleRoot`     | `string`                                  | Subdirectory of a module dir this target reads       |
| `categories`     | `Record<string, CategoryDeclaration>`     | Directory conventions to scan                        |
| `discover`       | `Record<string, DiscoverPattern>`         | Single files and globs                               |
| `registries`     | object                                    | Typed methods generated into `factories.ts`          |
| `callbackParams` | `Record<string, CallbackParamDefinition>` | Runtime proxies for callback methods                 |
| `transform`      | `(ctx) => void`                           | Adds imports, types and code before generation       |
| `generate`       | `(ctx) => CodegenTargetOutput`            | Replaces the template for this target                |
| `scaffolds`      | `Record<string, ScaffoldConfig>`          | `questpie add` templates                             |

<Callout type="warn" title="Contributors to one target must agree">
	Codegen merges every plugin's contribution to a target id. A disagreement on
	`root`, `outDir`, `outputFile` or `moduleRoot` throws, and so does a second
	`generate`. Categories merge per key, `factoryImports` arrays concatenate, and
	transforms run in plugin order.
</Callout>

## Categories

A category declares a directory to scan and how to emit what it finds.

```ts
categories: {
	views: {
		dirs: ["views"],
		prefix: "view",
		factoryFunctions: ["view"],
		registryKey: true,
		typeEmit: "standard",
	},
}
```

| Option               | Default              | Effect                                                        |
| -------------------- | -------------------- | ------------------------------------------------------------- |
| `dirs`               | required             | Scanned at the root and under `features/*/`                   |
| `prefix`             | required             | Variable prefix in the generated file, `_view_kanban`         |
| `recursive`          | `false`              | Descend into subdirectories                                   |
| `keySeparator`       | `"."`                | Joins recursive path segments, routes use `"/"`               |
| `emit`               | `"record"`           | `"array"` gives a flat list, as migrations and seeds use      |
| `typeEmit`           | `"standard"`         | Also `"services"`, `"emails"`, `"messages"`, `"none"`         |
| `createAppKey`       | the category name    | Emit under a different key, `emails` becomes `emailTemplates` |
| `registryKey`        | none                 | `true` uses the category name, a string overrides it          |
| `extractFromModules` | `true`               | Merge the same category from installed modules                |
| `extraTypeImports`   | none                 | Import statements added when the category has files           |
| `factoryFunctions`   | none                 | Names that mark an entity, turns on multi-entity files        |
| `factoryKeyStrategy` | `"factory-argument"` | `"export-or-filename"` keeps the argument out of the key      |
| `factoryArgument`    | none                 | Validate the argument, and require it to be unique            |
| `keyFromProperty`    | none                 | Key off a runtime property, blocks use `"state.name"`         |
| `keyFromSource`      | none                 | `"basename"` keys off the file name                           |
| `factoryImports`     | none                 | Named exports spread into the field defs in `factories.ts`    |
| `placeholder`        | none                 | Token in a `configType`, resolved to the union of keys        |
| `recordPlaceholder`  | none                 | Token resolved to the full record type                        |
| `typeRegistry`       | none                 | Interface to augment with the discovered names                |

`factoryFunctions` is the switch that matters most. With it, every exported
matching call becomes its own entity and files with no call are skipped. Without
it, one file is one entity.

## Single files and spreads

Where a category scans a directory, a `DiscoverPattern` claims a file.

```ts
discover: {
	myConfig: { pattern: "config/my-plugin.ts", configKey: "myPlugin" },
	fields: { pattern: "fields.ts", registryKey: "~fieldTypes" },
}
```

A bare string is shorthand. A pattern with a `*`, or with no extension, is a
directory. A plain path with an extension is a single file.

| Option          | Effect                                                         |
| --------------- | -------------------------------------------------------------- |
| `pattern`       | The path, relative to the discovery root                       |
| `resolve`       | `"auto"`, `"default"`, `"named"` or `"all"`. Default `"auto"`  |
| `keyFrom`       | `"filename"` or `"exportName"`                                 |
| `cardinality`   | `"single"` or `"map"`. Inferred from the pattern               |
| `mergeStrategy` | `"spread"` collects the root file plus every `features/*` copy |
| `registryKey`   | Adds `typeof` this file to the registry under the given key    |
| `configKey`     | Emits the whole file as one entry in the config bucket         |

`configKey` is the way to claim a config file. Pair it with an augmentation so
the key is typed.

```ts
declare module "questpie" {
	interface AppStateConfig {
		myPlugin?: { theme?: "light" | "dark" };
	}
}
```

Two modules that claim different config keys never collide. Two that claim the
same key do. The later one replaces the earlier one whole. Only `app`, `auth`
and `admin` merge below the key, so your own key is last-wins.

## Typed methods in `factories.ts`

Three declarations generate wrappers into the factory file.

`RegistryExtension` adds a method to `collection()`, `global()` or a field
instance. `stateKey` is where the value lands on the builder. `configType` is
the parameter type, and `imports` brings in what that type needs. Set
`isCallback` with `callbackContextParams` for a method that takes a callback.
Set `defaults` to fold your defaults under the user's config.

`SingletonFactory` generates a typed identity wrapper for a convention file.
Core declares two, `appConfig` and `authConfig`.

`BuilderFactory` generates a factory that needs the merged field definitions at
construction. The admin contributes `block` this way. That is why you import
`block` from `#questpie/factories` and not from a package.

A callback proxy always points at a real exported factory, never an inline
string. Core declares `f` as `createFieldNameProxy` from `questpie/builders`.
That is what makes `f.title` resolve to `"title"` inside `.fields()`.

## Transforms and custom generators

`transform(ctx)` runs after discovery and before generation. It can read every
discovered file and call `addImport`, `addTypeDeclaration`, `addRuntimeCode` and
`set`. The OpenAPI plugin uses one to emit a union of route keys. Sort what you
emit. Directory read order differs between machines, and unsorted output makes a
committed `.generated/` fail to match a fresh run on CI.

`generate(ctx)` replaces the template for a whole target. It returns `{ code,
additionalFiles? }`. Transforms have already run, so the extras are populated.
Every returned file is syntax-checked before it is written. Only one plugin per
target may supply it.

### Reuse the standard template

Writing a whole file by hand loses the emit rules the categories already
describe. `generateModuleTemplate` from `questpie/codegen` is the primitive the
built-in generator itself calls, so `generate()` can reuse it and change only
what it needs:

```ts
import { generateModuleTemplate } from "questpie/codegen";

generate(ctx) {
	const result = generateModuleTemplate({
		moduleName: "questpie-admin",
		discovered: ctx.discovered,
		categoryMeta: new Map(Object.entries(ctx.target.categories ?? {})),
		regenerateCommand: ctx.regenerateCommand,
		extraImports: ctx.extraImports,
	});
	return {
		code: result.code,
		additionalFiles: result.registriesCode
			? { "registries.ts": result.registriesCode }
			: undefined,
	};
}
```

It returns `{ code, registriesCode }`. `registriesCode` is `null` unless a
category augments a factory registry. Those augmentations go in a second file on
purpose. `module.ts` holds builder instances whose augmented interface points
back at the registry, so augmenting in the same file makes it circular.

`@questpie/admin` is the worked example. It calls this to write
`src/questpie/admin/.generated/client.ts` and adds only its own view and block
imports on top.

## Scaffolds

```ts
scaffolds: {
	widget: {
		dir: "widgets",
		extension: ".tsx",
		description: "A dashboard widget",
		template: ({ kebab, camel }) => `…`,
	},
}
```

`dir` is relative to the target root and `extension` defaults to `.ts`. The
template receives `kebab`, `camel`, `pascal`, `title` and `targetId`. Declaring
the same scaffold name on two targets is the point. `questpie add block hero`
writes the server definition and the client renderer in one go.

## Cross-target validators

A validator runs after every target has generated. It takes the map of results
and returns `ProjectionError[]`. One with `severity: "error"` fails the run and
the CLI exits `1`. A `"warning"` prints and passes. The admin uses one to catch
a server file naming a block the client target never registered.

## TypeScript

```ts
import type {
	CodegenPlugin,
	CodegenTargetContribution,
	CategoryDeclaration,
	DiscoverPattern,
	RegistryExtension,
	SingletonFactory,
	BuilderFactory,
	ScaffoldConfig,
	CodegenContext,
	CrossTargetValidator,
	ProjectionError,
} from "questpie/codegen";

import {
	categoryRecordEntry,
	importStatement,
	safeKey,
} from "questpie/codegen";
```

`questpie/codegen` is the entry point for plugin authors. It carries the types
above and the emit helpers `categoryRecordEntry`, `categoryTypeEntry`,
`importStatement`, `safeKey`, `sortedValues` and `sourceBasename`. It also
exports `generateModuleTemplate()`, which a target with its own `generate` calls
when `ctx.module` is set. To build a package's modules, put a
`questpie.config.ts` at the package root with `packageConfig()` and run
`questpie generate`. That is how the framework builds its own.

## Next

**[Building a plugin](/docs/guides/build-a-plugin)** walks the same contract as
a task. It also covers the smaller seams you reach for first, such as a custom
field type or a custom adapter.
