Building Generators

Actions Reference

Actions are the steps that change your project. They run in order, each with full access to the merged context — project signals, prompt answers, and config values.

Conditional execution

Every action supports an if field — a JavaScript expression evaluated against the merged context of signals + prompt answers. The action is skipped when falsy.

{
  "type": "template",
  "source": "templates/docker-compose.yml.hbs",
  "target": "docker-compose.yml",
  "if": "withDocker && packageManager === 'pnpm'"
}

Action types

copy

Copies a file from the generator directory to the target path verbatim — no rendering.

FieldTypeRequiredDescription
sourcestringYesPath relative to the generator directory
targetstringYesDestination path relative to project root
{
  "type": "copy",
  "source": "assets/logo.svg",
  "target": "public/logo.svg"
}
template

Renders a Handlebars template using the current context (signals + prompt answers) and writes the result to the target path.

FieldTypeRequiredDescription
sourcestringYesPath to .hbs file, relative to generator directory
targetstringYesDestination path. Supports {{variable}} interpolation
{
  "type": "template",
  "source": "templates/service.ts.hbs",
  "target": "src/{{kebabCase name}}/{{kebabCase name}}.service.ts"
}
Handlebars helpers (pascalCase, camelCase, kebabCase, snakeCase, capitalize) work in both template content and target paths.
append

Appends content to an existing file. Creates the file if it doesn't exist. Content supports Handlebars rendering.

FieldTypeRequiredDescription
targetstringYesPath to the file to append to
contentstringYesContent to append. Supports Handlebars rendering
newlinebooleanNoPrefix content with a newline (default: true)
{
  "type": "append",
  "target": ".env.example",
  "content": "DATABASE_URL=postgresql://localhost:5432/{{snakeCase name}}"
}
inject

Inserts content immediately after or before a marker string in an existing file. Throws if the marker is not found.

FieldTypeRequiredDescription
targetstringYesPath to the file to modify
contentstringYesContent to insert. Supports Handlebars rendering
afterstringNo*Insert after this marker string
beforestringNo*Insert before this marker string
{
  "type": "inject",
  "target": "src/app.module.ts",
  "after": "// GENERATORS",
  "content": "import { {{pascalCase name}}Module } from './{{kebabCase name}}/{{kebabCase name}}.module';"
}
Exactly one of after or before is required.
replace

Performs a regex find-and-replace inside a file. Search and replace strings support {{variable}} interpolation.

FieldTypeRequiredDescription
targetstringYesPath to the file to modify
searchstringYesRegex pattern to search for
replacestringYesReplacement string. Supports Handlebars and $1/$2 capture groups
flagsstringNoRegex flags (default: "g")
{
  "type": "replace",
  "target": "README.md",
  "search": "\$\{APP_NAME\}",
  "replace": "{{name}}",
  "flags": "g"
}
json

Merges a JSON object into an existing JSON file. Creates the file if it doesn't exist. Supports shallow (default) or deep merge.

FieldTypeRequiredDescription
targetstringYesPath to the JSON file
mergeobjectYesObject to merge into the file
deepbooleanNoUse deep merge instead of shallow (default: false)
{
  "type": "json",
  "target": "package.json",
  "deep": true,
  "merge": {
    "scripts": {
      "db:migrate": "prisma migrate dev",
      "db:push": "prisma db push"
    },
    "dependencies": {
      "@prisma/client": "^6.0.0"
    }
  }
}
With deep: false (default), top-level keys are replaced. With deep: true, nested objects are recursively merged.
env

Sets or updates a KEY=value line in a .env file. If the key already exists it is updated in place; otherwise it is appended.

FieldTypeRequiredDescription
keystringYesThe environment variable key
valuestringYesThe value. Supports {{variable}} interpolation
targetstringNoPath to the env file (default: .env)
{
  "type": "env",
  "key": "DATABASE_URL",
  "value": "postgresql://localhost:5432/{{snakeCase name}}_dev"
}
ast-add-import

Adds a TypeScript import declaration using AST manipulation (ts-morph). Never duplicates an existing import.

FieldTypeRequiredDescription
targetstringYesPath to the TypeScript file
fromstringYesThe module specifier (e.g. '@nestjs/common')
importstring | string[]YesNamed import(s) to add
isDefaultbooleanNoAdd as default import instead of named
{
  "type": "ast-add-import",
  "target": "src/app.module.ts",
  "from": "@nestjs/config",
  "import": ["ConfigModule", "ConfigService"]
}
If the module is already imported, only the missing named imports are added — existing ones are untouched.
command

Runs a shell command in the project directory. Streams output to the terminal.

FieldTypeRequiredDescription
commandstringYesThe shell command to run. Supports {{variable}} interpolation
cwdstringNoSubdirectory to run in, relative to project root
{
  "type": "command",
  "command": "pnpm add {{dependencies}}"
}
Runs with shell: true so shell syntax (&&, pipes) works. The generator fails if exit code is non-zero.
script

Runs a script file from the generator directory. Useful for complex post-install logic.

FieldTypeRequiredDescription
scriptstringYesPath to the script file, relative to generator directory
cwdstringNoSubdirectory to run in, relative to project root
{
  "type": "script",
  "script": "scripts/postinstall.sh"
}
xo-call

Invokes another generator from within this one. Useful for composing generators without requiring the user to run them separately.

FieldTypeRequiredDescription
generatorstringYesThe generator name to invoke (same resolution rules as xo add)
{
  "type": "xo-call",
  "generator": "acme/eslint-setup"
}
The nested generator runs with the same cwd and context. Its prompts are run interactively before its actions execute.

Dry run mode

Pass --dry-run to any CLI command to preview actions without writing files or running commands. Each action prints a log line describing what it would do.

$ xo add acme/auth-jwt --dry-run

[dry-run] template: templates/auth.service.ts.hbs → src/auth/auth.service.ts
[dry-run] template: templates/jwt.guard.ts.hbs → src/auth/jwt.guard.ts
[dry-run] ast-add-import: JwtModule from @nestjs/jwt in src/app.module.ts
[dry-run] json: package.json
[dry-run] command: pnpm install