Building Generators
Signals Reference
Before any generator runs, xo scans the project and builds a flat key-value signal map. Signals describe what the project looks like — which files exist, which packages are installed, which framework is detected. Generators use signals to enforce compatibility and adapt their behavior.
How signal scanning works
xo's signal scanner is framework-agnostic. It scans well-known files and derives higher-level signals from them. Framework knowledge lives in generators through their detects rules — the engine stays dumb.
A typical Next.js + TypeScript project signal map:
{
"file:package.json": true,
"file:tsconfig.json": true,
"file:tailwind.config.ts": true,
"file:prisma/schema.prisma": false,
"pkg:react": true,
"pkg:next": true,
"pkg:typescript": true,
"script:build": true,
"script:dev": true,
"framework": "nextjs",
"packageManager": "pnpm",
"language": "typescript",
"isMonorepo": false
}Signal categories
file: prefix
true or false depending on whether the file exists in the project root. Checked files include: package.json, tsconfig.json, pnpm-workspace.yaml, turbo.json, tailwind.config.ts/js, next.config.ts/js, vite.config.ts/js, vitest.config.ts, jest.config.ts/js, Dockerfile, docker-compose.yml/yaml, .env, .env.example, prisma/schema.prisma, prisma.config.ts, drizzle.config.ts, and ESLint / Prettier config files.
pkg: prefix
true for every package in dependencies, devDependencies, and peerDependencies.
{ "signal": "pkg:@nestjs/core", "exists": true } // NestJS project
{ "signal": "pkg:drizzle-orm", "exists": false } // NOT using Drizzlescript: prefix
true for every script name defined in package.json scripts.
Derived signals
| Signal | Type | Possible values |
|---|---|---|
| framework | string | undefined | nextjs, nuxt, sveltekit, react, vue, svelte, nestjs, express, fastify |
| packageManager | string | pnpm, bun, yarn, npm |
| language | string | typescript, javascript |
| isMonorepo | boolean | true if pnpm-workspace.yaml or package.json workspaces present |
Using signals in detects
The detects array lets you declare which project types a generator is compatible with. All rules must pass — they are ANDed together.
{
"name": "acme/prisma-setup",
"type": "feature",
"detects": [
{ "signal": "language", "equals": "typescript" },
{ "signal": "packageManager", "matches": "^(npm|pnpm)$" },
{ "signal": "file:prisma/schema.prisma", "exists": false }
],
"actions": [...]
}| Field | Type | Meaning |
|---|---|---|
| signal | string | The signal key to check (e.g. pkg:react, framework) |
| exists | boolean | Signal must be truthy (true) or absent/falsy (false) |
| equals | string | Signal value must exactly equal this string |
| matches | string | Signal value must match this regex pattern |
Generator blocked if detects fails
If any detect rule fails, xo refuses to run the generator with a clear error — it does not silently skip it.Using signals in prompts and actions
Signals are merged into the context alongside prompt answers, so they're available in when (prompt conditions) and if (action conditions).
In a prompt when field:
{
"name": "addDockerCompose",
"type": "confirm",
"message": "Add docker-compose.yml?",
"when": "isMonorepo === false && framework === 'nestjs'"
}In an action if field:
{
"type": "template",
"source": "templates/jest.config.ts.hbs",
"target": "jest.config.ts",
"if": "language === 'typescript'"
}Package signals in expressions
Inif / when expressions, keys like pkg:react are not valid JS identifiers. Use { signal: 'pkg:react', exists: true } in detects for package checks. For prompt/action conditions, use answers or derived signals like framework and language instead.Using signals in templates
Signals are also available as Handlebars variables inside template files, letting content adapt to the project automatically.
// adapts to project setup automatically
export default {
{{#if (eq packageManager "pnpm")}}
packageManager: "pnpm",
{{/if}}
framework: "{{framework}}",
language: "{{language}}",
};