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.sh

Template 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.

generator.jsonjson
{
  "name": "acme/hello-world",
  "type": "feature",
  "actions": [
    {
      "type": "template",
      "source": "templates/hello.ts.hbs",
      "target": "src/hello.ts"
    }
  ]
}
templates/hello.ts.hbshandlebars
// 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

FieldTypeRequiredDescription
namestringYesUnique name, conventionally owner/repo
type"project" | "feature"Yesproject = new scaffold, feature = adds to existing
requiresstring[]NoGenerators that must already be applied
conflictsstring[]NoGenerators that must NOT be present
providesstring[]NoLogical capabilities this generator declares
detectsDetectRule[]NoSignal rules that must match before running
promptsPrompt[]NoInteractive questions asked before running actions
actionsAction[]NoOrdered 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.

TypeRenders asExtra fields
inputFree-text field
confirmYes / No question
selectSingle-choice listchoices: string[]
multiselectMulti-choice checkboxeschoices: 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.

templates/component.tsx.hbshandlebars
"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>
  );
}
HelperInputOutput
pascalCasemy-componentMyComponent
camelCasemy-componentmyComponent
kebabCaseMyComponentmy-component
snakeCaseMyComponentmy_component
capitalizehello worldHello 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:

generator.jsonjson
{
  "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 an if 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.

terminal
$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:

terminal
$xo add acme/react-component

For a generator in a subdirectory, use owner/repo/subpath:

terminal
$xo add acme/generators/react-component

Private generators

Generators stored under .xo/generators/ in your project are resolved locally first — no GitHub fetch needed.