# Building Custom Generators

This guide teaches you how to create custom code generators to extend Bangstack with your own technology stacks, frameworks, and patterns.

## Table of Contents

1. [Why Custom Generators?](#why-custom-generators)
2. [Generator Architecture](#generator-architecture)
3. [Your First Generator](#your-first-generator)
4. [Generator API Reference](#generator-api-reference)
5. [Advanced Patterns](#advanced-patterns)
6. [Testing Your Generator](#testing-your-generator)
7. [Publishing Your Generator](#publishing-your-generator)

## Why Custom Generators?

Bangstack comes with built-in generators for common stacks (React, Express, etc.), but you might want to:

- Support your company's custom frameworks
- Generate code for niche technologies (Elixir, Rust, etc.)
- Create domain-specific generators (fintech, healthcare, etc.)
- Implement your team's coding standards
- Automate repetitive boilerplate in your projects

Custom generators let you extend Bangstack to fit your exact needs!

## Generator Architecture

### How Generators Work

```
Workflow YAML
      ↓
   Parser (validates YAML)
      ↓
   Engine (processes workflow)
      ↓
   Generators (generate code)
      ↓
   File Writer (writes to disk)
```

### Generator Interface

Every generator implements this interface:

```typescript
interface Generator {
  // Unique identifier
  id: string;

  // Human-readable name
  name: string;

  // Description of what this generator does
  description: string;

  // Main generation function
  generate(context: GeneratorContext): GenerationResult;

  // Optional: Check if this generator applies to the workflow
  canGenerate?(context: GeneratorContext): boolean;
}
```

### Generator Context

Generators receive all the information they need:

```typescript
interface GeneratorContext {
  // Parsed workflow (legacy format)
  workflow: Workflow;

  // Parsed extended workflow (new format)
  extendedWorkflow?: ExtendedWorkflow;

  // Output directory
  outputDirectory: string;

  // Additional options
  options?: {
    dryRun?: boolean;
    verbose?: boolean;
    // ... other CLI flags
  };
}
```

### Generation Result

Generators return files to be written:

```typescript
interface GenerationResult {
  // Files to generate
  files: GeneratedFile[];

  // Optional warnings
  warnings?: string[];

  // Optional errors (non-fatal)
  errors?: string[];
}

interface GeneratedFile {
  // Relative path from output directory
  path: string;

  // File content
  content: string;

  // Optional: Prevent overwriting existing files
  skipIfExists?: boolean;
}
```

## Your First Generator

Let's build a generator for Fastify (a fast Node.js web framework)!

### Step 1: Create Generator File

Create `src/generators/backend/fastify-generator.ts`:

```typescript
import { Generator, GeneratorContext, GenerationResult, GeneratedFile } from '../types';
import * as path from 'path';

export class FastifyGenerator implements Generator {
  id = 'fastify';
  name = 'Fastify Backend Generator';
  description = 'Generates a Fastify backend with TypeScript';

  canGenerate(context: GeneratorContext): boolean {
    const { workflow, extendedWorkflow } = context;

    // Check if workflow uses Fastify
    if (extendedWorkflow?.technology?.backend?.type === 'fastify') {
      return true;
    }

    // Legacy format check
    if (workflow.stack.backend?.technology === 'fastify') {
      return true;
    }

    return false;
  }

  generate(context: GeneratorContext): GenerationResult {
    const files: GeneratedFile[] = [];

    // Get backend directory
    const backendDir = this.getBackendDirectory(context);

    // Generate files
    files.push(this.generateMainServer(backendDir));
    files.push(this.generatePackageJson(backendDir));
    files.push(this.generateTsConfig(backendDir));
    files.push(this.generateEnvFile(backendDir));
    files.push(this.generateRoutes(backendDir, context));

    return { files };
  }

  private getBackendDirectory(context: GeneratorContext): string {
    // Check extended workflow first
    if (context.extendedWorkflow?.technology?.backend) {
      return (context.extendedWorkflow.technology.backend as any).directory || 'backend';
    }

    // Fall back to legacy
    return 'backend';
  }

  private generateMainServer(backendDir: string): GeneratedFile {
    const content = `
import Fastify from 'fastify';
import cors from '@fastify/cors';
import helmet from '@fastify/helmet';
import dotenv from 'dotenv';

dotenv.config();

const fastify = Fastify({
  logger: true,
});

// Plugins
fastify.register(cors, {
  origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
});

fastify.register(helmet, {
  contentSecurityPolicy: false,
});

// Routes
fastify.get('/health', async (request, reply) => {
  return { status: 'ok', timestamp: new Date().toISOString() };
});

// Start server
const start = async () => {
  try {
    const port = parseInt(process.env.PORT || '3001', 10);
    await fastify.listen({ port, host: '0.0.0.0' });
    console.log(\`Server listening on port \${port}\`);
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();
`.trim();

    return {
      path: path.join(backendDir, 'src', 'index.ts'),
      content,
    };
  }

  private generatePackageJson(backendDir: string): GeneratedFile {
    const content = JSON.stringify({
      name: 'fastify-backend',
      version: '1.0.0',
      scripts: {
        dev: 'tsx watch src/index.ts',
        build: 'tsc',
        start: 'node dist/index.js',
        test: 'vitest',
      },
      dependencies: {
        fastify: '^4.25.0',
        '@fastify/cors': '^9.0.0',
        '@fastify/helmet': '^11.0.0',
        dotenv: '^16.0.0',
      },
      devDependencies: {
        '@types/node': '^20.0.0',
        typescript: '^5.0.0',
        tsx: '^4.0.0',
        vitest: '^1.0.0',
      },
    }, null, 2);

    return {
      path: path.join(backendDir, 'package.json'),
      content,
    };
  }

  private generateTsConfig(backendDir: string): GeneratedFile {
    const content = JSON.stringify({
      compilerOptions: {
        target: 'ES2022',
        module: 'commonjs',
        lib: ['ES2022'],
        outDir: './dist',
        rootDir: './src',
        strict: true,
        esModuleInterop: true,
        skipLibCheck: true,
        forceConsistentCasingInFileNames: true,
      },
      include: ['src/**/*'],
      exclude: ['node_modules', 'dist'],
    }, null, 2);

    return {
      path: path.join(backendDir, 'tsconfig.json'),
      content,
    };
  }

  private generateEnvFile(backendDir: string): GeneratedFile {
    const content = `
PORT=3001
CORS_ORIGIN=http://localhost:3000
NODE_ENV=development
`.trim();

    return {
      path: path.join(backendDir, '.env.example'),
      content,
      skipIfExists: true,
    };
  }

  private generateRoutes(backendDir: string, context: GeneratorContext): GeneratedFile {
    const { extendedWorkflow } = context;
    let content = `
import { FastifyInstance } from 'fastify';

export async function registerRoutes(fastify: FastifyInstance) {
`;

    // Generate routes from models
    if (extendedWorkflow?.models) {
      for (const [modelName, model] of Object.entries(extendedWorkflow.models)) {
        const pluralName = modelName.toLowerCase() + 's';

        content += `
  // ${modelName} routes
  fastify.get('/${pluralName}', async (request, reply) => {
    // TODO: Implement list ${pluralName}
    return [];
  });

  fastify.get('/${pluralName}/:id', async (request, reply) => {
    // TODO: Implement get ${modelName}
    return {};
  });

  fastify.post('/${pluralName}', async (request, reply) => {
    // TODO: Implement create ${modelName}
    return {};
  });

  fastify.put('/${pluralName}/:id', async (request, reply) => {
    // TODO: Implement update ${modelName}
    return {};
  });

  fastify.delete('/${pluralName}/:id', async (request, reply) => {
    // TODO: Implement delete ${modelName}
    reply.status(204).send();
  });
`;
      }
    }

    content += `
}
`.trim();

    return {
      path: path.join(backendDir, 'src', 'routes.ts'),
      content,
    };
  }
}
```

### Step 2: Register Your Generator

Add to `src/generators/index.ts`:

```typescript
// Import your generator
import { FastifyGenerator } from './backend/fastify-generator';

// Register it
const fastifyGenerator = new FastifyGenerator();
globalRegistry.registerBackendGenerator(fastifyGenerator);
```

### Step 3: Test It!

Create a test workflow `test-fastify.workflow.yaml`:

```yaml
workflow:
  id: test-fastify
  version: 1.0.0
  name: Test Fastify
  description: Test Fastify generator

technology:
  backend:
    type: fastify
    directory: backend

models:
  User:
    description: User model
    fields:
      - name: id
        type: uuid
        required: true
      - name: email
        type: string
        required: true
```

Generate:

```bash
npm run build
bang generate test-fastify.workflow.yaml --output ./test-output
```

Check `test-output/backend/` - you should see your generated Fastify app!

## Generator API Reference

### Base Generator Class

For simpler generators, extend the base class:

```typescript
import { CodeGenerator } from './code-generator';

export class MyGenerator extends CodeGenerator {
  constructor() {
    super({
      id: 'my-generator',
      name: 'My Generator',
      description: 'Generates my custom code',
    });
  }

  async generateCode(context: GeneratorContext): Promise<GenerationResult> {
    // Your generation logic
    return { files: [] };
  }
}
```

### Utility Functions

Bangstack provides helper functions:

```typescript
import {
  toPascalCase,
  toCamelCase,
  toSnakeCase,
  toKebabCase,
  pluralize,
  singularize,
} from '../utils/string-helpers';

// Convert naming conventions
toPascalCase('user_profile'); // => 'UserProfile'
toCamelCase('user_profile');  // => 'userProfile'
toSnakeCase('UserProfile');   // => 'user_profile'
toKebabCase('UserProfile');   // => 'user-profile'

// Pluralization
pluralize('user');   // => 'users'
singularize('users'); // => 'user'
```

### Template Engine

Use the built-in template engine:

```typescript
import { TemplateEngine } from '../template-engine';

const template = `
export interface {{modelName}} {
  {{#each fields}}
  {{name}}: {{type}};
  {{/each}}
}
`;

const result = TemplateEngine.render(template, {
  modelName: 'User',
  fields: [
    { name: 'id', type: 'string' },
    { name: 'email', type: 'string' },
  ],
});
```

### File Utilities

```typescript
import { FileWriter } from '../file-writer';

// Check if file exists before generating
if (!FileWriter.fileExists(outputPath)) {
  files.push({
    path: outputPath,
    content: generatedCode,
  });
}

// Read existing file
const existingContent = FileWriter.readFile(existingPath);

// Merge with existing code
const mergedContent = FileWriter.mergeCode(existingContent, newCode);
```

## Advanced Patterns

### Multi-File Generation

Generate multiple related files:

```typescript
generate(context: GeneratorContext): GenerationResult {
  const files: GeneratedFile[] = [];
  const { models } = context.extendedWorkflow || {};

  if (!models) {
    return { files, warnings: ['No models found'] };
  }

  // Generate one file per model
  for (const [modelName, model] of Object.entries(models)) {
    files.push(this.generateModel(modelName, model));
    files.push(this.generateController(modelName, model));
    files.push(this.generateService(modelName, model));
    files.push(this.generateTest(modelName, model));
  }

  // Generate index file that imports all
  files.push(this.generateIndex(Object.keys(models)));

  return { files };
}
```

### Conditional Generation

Generate different code based on configuration:

```typescript
generate(context: GeneratorContext): GenerationResult {
  const files: GeneratedFile[] = [];
  const { workflow, extendedWorkflow } = context;

  // Check if authentication is enabled
  const hasAuth = extendedWorkflow?.authentication?.enabled;
  if (hasAuth) {
    files.push(this.generateAuthMiddleware());
    files.push(this.generateAuthRoutes());
  }

  // Check if database is configured
  const hasDatabase = extendedWorkflow?.technology?.database;
  if (hasDatabase) {
    files.push(this.generateDatabaseConfig());
    files.push(this.generateMigrations());
  }

  // Check if cache is configured
  const hasCache = extendedWorkflow?.technology?.cache;
  if (hasCache) {
    files.push(this.generateCacheClient());
  }

  return { files };
}
```

### Code Formatting

Always format generated code:

```typescript
import prettier from 'prettier';

private formatCode(code: string, parser: 'typescript' | 'babel' | 'json' = 'typescript'): string {
  try {
    return prettier.format(code, {
      parser,
      singleQuote: true,
      trailingComma: 'es5',
      printWidth: 100,
      tabWidth: 2,
    });
  } catch (error) {
    // Return unformatted if prettier fails
    console.warn('Failed to format code:', error);
    return code;
  }
}

// Use it:
const code = this.generateCode();
const formatted = this.formatCode(code);

return {
  path: 'output.ts',
  content: formatted,
};
```

### Incremental Updates

Support updating existing generated files:

```typescript
generate(context: GeneratorContext): GenerationResult {
  const files: GeneratedFile[] = [];
  const outputPath = path.join(context.outputDirectory, 'routes.ts');

  // Check if file already exists
  if (FileWriter.fileExists(outputPath)) {
    const existing = FileWriter.readFile(outputPath);

    // Parse existing code
    const existingRoutes = this.parseRoutes(existing);

    // Generate only new routes
    const newRoutes = this.generateNewRoutes(context, existingRoutes);

    // Merge
    const merged = this.mergeRoutes(existing, newRoutes);

    files.push({
      path: 'routes.ts',
      content: merged,
    });
  } else {
    // Generate fresh file
    files.push({
      path: 'routes.ts',
      content: this.generateAllRoutes(context),
    });
  }

  return { files };
}
```

### Error Handling

Provide helpful error messages:

```typescript
generate(context: GeneratorContext): GenerationResult {
  const files: GeneratedFile[] = [];
  const errors: string[] = [];
  const warnings: string[] = [];

  const { extendedWorkflow } = context;

  // Validate requirements
  if (!extendedWorkflow?.models || Object.keys(extendedWorkflow.models).length === 0) {
    errors.push('No models defined. This generator requires at least one model.');
    return { files, errors };
  }

  const databaseType = extendedWorkflow.technology?.database?.type;
  if (!databaseType) {
    warnings.push('No database configured. Generated code will need a database to run.');
  }

  if (!['postgresql', 'mysql'].includes(databaseType || '')) {
    errors.push(`Unsupported database type: ${databaseType}. This generator only supports PostgreSQL and MySQL.`);
    return { files, errors };
  }

  try {
    // Generate files
    files.push(...this.generateFiles(context));
  } catch (error) {
    errors.push(`Generation failed: ${(error as Error).message}`);
  }

  return { files, warnings, errors };
}
```

## Testing Your Generator

### Unit Tests

Create `fastify-generator.test.ts`:

```typescript
import { describe, it, expect } from 'vitest';
import { FastifyGenerator } from './fastify-generator';
import { GeneratorContext } from '../types';

describe('FastifyGenerator', () => {
  const generator = new FastifyGenerator();

  it('should have correct metadata', () => {
    expect(generator.id).toBe('fastify');
    expect(generator.name).toBe('Fastify Backend Generator');
  });

  it('should generate for Fastify workflows', () => {
    const context: GeneratorContext = {
      workflow: { /* ... */ },
      extendedWorkflow: {
        technology: {
          backend: { type: 'fastify' },
        },
      },
      outputDirectory: './output',
    };

    expect(generator.canGenerate(context)).toBe(true);
  });

  it('should not generate for non-Fastify workflows', () => {
    const context: GeneratorContext = {
      workflow: { /* ... */ },
      extendedWorkflow: {
        technology: {
          backend: { type: 'express' },
        },
      },
      outputDirectory: './output',
    };

    expect(generator.canGenerate(context)).toBe(false);
  });

  it('should generate main server file', () => {
    const context: GeneratorContext = {
      workflow: { /* ... */ },
      extendedWorkflow: {
        technology: {
          backend: { type: 'fastify' },
        },
      },
      outputDirectory: './output',
    };

    const result = generator.generate(context);

    expect(result.files).toHaveLength(5);
    expect(result.files[0].path).toContain('index.ts');
    expect(result.files[0].content).toContain('Fastify');
  });

  it('should generate routes from models', () => {
    const context: GeneratorContext = {
      workflow: { /* ... */ },
      extendedWorkflow: {
        technology: {
          backend: { type: 'fastify' },
        },
        models: {
          User: {
            description: 'User model',
            fields: [
              { name: 'id', type: 'uuid' },
              { name: 'email', type: 'string' },
            ],
          },
        },
      },
      outputDirectory: './output',
    };

    const result = generator.generate(context);
    const routesFile = result.files.find(f => f.path.includes('routes.ts'));

    expect(routesFile).toBeDefined();
    expect(routesFile!.content).toContain('users');
    expect(routesFile!.content).toContain('fastify.get');
    expect(routesFile!.content).toContain('fastify.post');
  });
});
```

Run tests:

```bash
npm test fastify-generator
```

### Integration Tests

Test the full generation flow:

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { CodeGenerator } from '../code-generator';
import { FastifyGenerator } from './fastify-generator';

describe('FastifyGenerator Integration', () => {
  const outputDir = path.join(__dirname, 'test-output');

  beforeEach(() => {
    // Clean output directory
    if (fs.existsSync(outputDir)) {
      fs.rmSync(outputDir, { recursive: true });
    }
    fs.mkdirSync(outputDir, { recursive: true });
  });

  afterEach(() => {
    // Cleanup
    if (fs.existsSync(outputDir)) {
      fs.rmSync(outputDir, { recursive: true });
    }
  });

  it('should generate a working Fastify app', () => {
    const generator = new FastifyGenerator();
    const context = {
      workflow: { /* ... */ },
      extendedWorkflow: {
        technology: {
          backend: { type: 'fastify', directory: 'backend' },
        },
        models: {
          User: {
            fields: [
              { name: 'id', type: 'uuid' },
              { name: 'email', type: 'string' },
            ],
          },
        },
      },
      outputDirectory: outputDir,
    };

    const result = generator.generate(context);

    // Write files
    for (const file of result.files) {
      const filePath = path.join(outputDir, file.path);
      fs.mkdirSync(path.dirname(filePath), { recursive: true });
      fs.writeFileSync(filePath, file.content);
    }

    // Verify files exist
    expect(fs.existsSync(path.join(outputDir, 'backend', 'src', 'index.ts'))).toBe(true);
    expect(fs.existsSync(path.join(outputDir, 'backend', 'package.json'))).toBe(true);

    // Verify package.json is valid JSON
    const packageJson = JSON.parse(
      fs.readFileSync(path.join(outputDir, 'backend', 'package.json'), 'utf-8')
    );
    expect(packageJson.dependencies).toHaveProperty('fastify');
  });
});
```

## Publishing Your Generator

### As an NPM Package

Create `package.json`:

```json
{
  "name": "@yourcompany/bangstack-fastify-generator",
  "version": "1.0.0",
  "description": "Fastify generator for Bangstack",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "test": "vitest"
  },
  "peerDependencies": {
    "bangstack": "^1.0.0"
  },
  "keywords": ["bangstack", "generator", "fastify"],
  "author": "Your Name",
  "license": "MIT"
}
```

Export your generator:

```typescript
// src/index.ts
export { FastifyGenerator } from './fastify-generator';
```

Publish:

```bash
npm publish --access public
```

### Usage by Others

Users install your generator:

```bash
npm install @yourcompany/bangstack-fastify-generator
```

Register it in their project:

```typescript
// custom-generators.ts
import { globalRegistry } from 'bangstack';
import { FastifyGenerator } from '@yourcompany/bangstack-fastify-generator';

const fastifyGenerator = new FastifyGenerator();
globalRegistry.registerBackendGenerator(fastifyGenerator);
```

## Real-World Examples

### Example 1: Python FastAPI Generator

```typescript
export class FastAPIGenerator implements Generator {
  id = 'fastapi';
  name = 'FastAPI Backend Generator';
  description = 'Generates a Python FastAPI backend';

  generate(context: GeneratorContext): GenerationResult {
    const files: GeneratedFile[] = [];

    files.push(this.generateMain());
    files.push(this.generateModels(context));
    files.push(this.generateRouters(context));
    files.push(this.generateRequirements());

    return { files };
  }

  private generateMain(): GeneratedFile {
    return {
      path: 'backend/main.py',
      content: `
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/health")
async def health_check():
    return {"status": "ok"}
`.trim(),
    };
  }

  private generateRequirements(): GeneratedFile {
    return {
      path: 'backend/requirements.txt',
      content: `
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.0
sqlalchemy==2.0.0
`.trim(),
    };
  }

  // ... more methods
}
```

### Example 2: Flutter Mobile Generator

```typescript
export class FlutterGenerator implements Generator {
  id = 'flutter';
  name = 'Flutter Mobile Generator';
  description = 'Generates a Flutter mobile app';

  generate(context: GeneratorContext): GenerationResult {
    const files: GeneratedFile[] = [];

    files.push(this.generatePubspec());
    files.push(this.generateMainDart());
    files.push(this.generateModels(context));
    files.push(this.generateServices(context));
    files.push(this.generateScreens(context));

    return { files };
  }

  private generatePubspec(): GeneratedFile {
    return {
      path: 'mobile/pubspec.yaml',
      content: `
name: mobile_app
description: Generated Flutter app
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  provider: ^6.1.0
`.trim(),
    };
  }

  // ... more methods
}
```

## Best Practices

### 1. Always Validate Input

```typescript
generate(context: GeneratorContext): GenerationResult {
  // Validate before generating
  const validation = this.validateContext(context);
  if (!validation.valid) {
    return {
      files: [],
      errors: validation.errors,
    };
  }

  // Proceed with generation
  // ...
}
```

### 2. Use TypeScript for Type Safety

```typescript
// Define clear types for your configuration
interface FastifyConfig {
  port: number;
  cors: {
    origin: string;
  };
  logging: boolean;
}

// Use them in your generator
private getConfig(context: GeneratorContext): FastifyConfig {
  const config = context.extendedWorkflow?.technology?.backend?.config as FastifyConfig;
  return config || this.getDefaultConfig();
}
```

### 3. Provide Good Error Messages

```typescript
if (!models || Object.keys(models).length === 0) {
  return {
    files: [],
    errors: [
      'No models found in workflow.',
      'This generator requires at least one model to generate code.',
      'Add models to your workflow or use a different generator.',
    ],
  };
}
```

### 4. Make Generated Code Readable

```typescript
// Add helpful comments
const content = `
/**
 * ${modelName} Controller
 * Generated by Bangstack ${new Date().toISOString()}
 *
 * This file was automatically generated. You can safely modify it.
 * To regenerate, run: bang generate workflow.yaml
 */

import { FastifyInstance } from 'fastify';
// ...
`;
```

### 5. Test Thoroughly

- Unit test each method
- Integration test the full flow
- Test edge cases (no models, invalid config, etc.)
- Test with real workflows

## Next Steps

- Read [Generator Framework Guide](../GENERATOR-FRAMEWORK.md) for advanced topics
- Check out [existing generators](https://github.com/mogullabs/bangstack/tree/main/src/generators) for more examples
- Join our [Discord](https://discord.gg/bangstack) to discuss generator development
- Share your generator with the community!

## Resources

- [Bangstack Generator API Docs](../API-REFERENCE.md)
- [Extended Schema Guide](../EXTENDED-SCHEMA-GUIDE.md)
- [Workflow Best Practices](../WORKFLOW-BEST-PRACTICES.md)

---

**Need help?** Open an issue on [GitHub](https://github.com/mogullabs/bangstack/issues) or ask in our [Discord](https://discord.gg/bangstack)
