Building Generators
Creating a Generator
A generator is a folder with a generator.json manifest and any template files it references. This page walks through everything from a minimal example to a production-ready generator.
Folder structure
There is no required file organization beyond generator.json at the root.
my-generator/
generator.json ← required manifest
templates/ ← your template files
index.tsx.hbs
styles.css.hbs
README.md.hbs
scripts/ ← optional post-install scripts
setup.shTemplate files can use any extension — .hbs is conventional but not required. xo renders any file referenced by a template action.
Minimal example
The smallest valid generator needs only name, type, and at least one action.
{
"name": "acme/hello-world",
"type": "feature",
"actions": [
{
"type": "template",
"source": "templates/hello.ts.hbs",
"target": "src/hello.ts"
}
]
}// Generated by xo — acme/hello-world
export function hello(name: string): string {
return `Hello, {{name}}!`;
}Variables in template files
Inside.hbs files, use {{variableName}} — values come from prompt answers and project signals.generator.json reference
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Unique name, conventionally owner/repo |
| type | "project" | "feature" | Yes | project = new scaffold, feature = adds to existing |
| requires | string[] | No | Generators that must already be applied |
| conflicts | string[] | No | Generators that must NOT be present |
| provides | string[] | No | Logical capabilities this generator declares |
| detects | DetectRule[] | No | Signal rules that must match before running |
| prompts | Prompt[] | No | Interactive questions asked before running actions |
| actions | Action[] | No | Ordered list of file/command operations |
Defining prompts
Prompts collect user input before actions run. Answers become template variables available in all action fields and template files.
| Type | Renders as | Extra fields |
|---|---|---|
| input | Free-text field | — |
| confirm | Yes / No question | — |
| select | Single-choice list | choices: string[] |
| multiselect | Multi-choice checkboxes | choices: string[] |
Use the when field to conditionally show a prompt based on prior answers or signals:
{
"name": "withTests",
"type": "confirm",
"message": "Add a test file?",
"when": "componentType !== 'server'"
}Handlebars templates
Template files use Handlebars syntax. Variables come from prompt answers and project signals. Helpers are also available in target path values.
"use client";
import type { ComponentProps } from "react";
interface {{pascalCase componentName}}Props extends ComponentProps<"div"> {
// your props here
}
export function {{pascalCase componentName}}({ className, ...props }: {{pascalCase componentName}}Props) {
return (
<div className={className} {...props}>
{{pascalCase componentName}}
</div>
);
}| Helper | Input | Output |
|---|---|---|
| pascalCase | my-component | MyComponent |
| camelCase | my-component | myComponent |
| kebabCase | MyComponent | my-component |
| snakeCase | MyComponent | my_component |
| capitalize | hello world | Hello world |
| eq / ne | {{#if (eq a b)}} | boolean equal / not-equal |
| or / and | {{#if (or a b)}} | logical or / and |
Detect rules
Use detects to ensure your generator only runs in compatible projects. All rules are ANDed — every one must pass.
"detects": [
{ "signal": "pkg:react", "exists": true },
{ "signal": "language", "equals": "typescript" },
{ "signal": "framework", "matches": "^(react|nextjs)$" }
]See the Signals Reference for the full list of available signals.
Full working example
A complete generator with prompts, conditional actions, detects, requires, and conflicts:
{
"name": "acme/react-component",
"type": "feature",
"requires": ["acme/react-setup"],
"conflicts": ["acme/vue-setup"],
"detects": [
{ "signal": "pkg:react", "exists": true },
{ "signal": "language", "equals": "typescript" }
],
"prompts": [
{
"name": "componentName",
"type": "input",
"message": "What is the component name?"
},
{
"name": "componentType",
"type": "select",
"message": "Component type?",
"choices": ["client", "server", "shared"]
},
{
"name": "withStories",
"type": "confirm",
"message": "Add a Storybook story?",
"when": "componentType === 'client' || componentType === 'shared'"
},
{
"name": "withTests",
"type": "confirm",
"message": "Add a test file?"
}
],
"actions": [
{
"type": "template",
"source": "templates/component.tsx.hbs",
"target": "src/components/{{pascalCase componentName}}/index.tsx"
},
{
"type": "template",
"source": "templates/stories.tsx.hbs",
"target": "src/components/{{pascalCase componentName}}/{{pascalCase componentName}}.stories.tsx",
"if": "withStories"
},
{
"type": "template",
"source": "templates/test.tsx.hbs",
"target": "src/components/{{pascalCase componentName}}/index.test.tsx",
"if": "withTests"
},
{
"type": "inject",
"target": "src/components/index.ts",
"after": "// components",
"content": "export { {{pascalCase componentName}} } from './{{pascalCase componentName}}';"
}
]
}The if field
Every action supports anif field — a JavaScript expression evaluated against the merged context of signals + prompt answers. The action is skipped when falsy.Publishing to GitHub
Push your generator to any public GitHub repo — no registry account required.
$git init && git add .$git commit -m "feat: react-component generator"$git remote add origin git@github.com:acme/react-component.git$git push -u origin main
Users then run:
$xo add acme/react-component
For a generator in a subdirectory, use owner/repo/subpath:
$xo add acme/generators/react-component
Private generators
Generators stored under.xo/generators/ in your project are resolved locally first — no GitHub fetch needed.