Skip to content

Latest commit

 

History

History

README.md

@auto-engineer/component-implementor-react

Generates production-ready React components, tests, and Storybook stories from structured spec deltas using an LLM-powered multi-stage pipeline.

Purpose

Without this package, you would have to manually write every React component, its Vitest test file, and its Storybook story from scratch, then iterate through type errors, test failures, and lint issues by hand. This package automates the entire flow: it scaffolds the component skeleton, calls an LLM to fill in the implementation body, generates tests and stories in parallel, runs type-check/test/lint verification loops, and scores the result -- all in a single command invocation.

Command

Name Alias Description
ImplementComponent implement:component Generate a React component with test and Storybook story from structured spec deltas

Events Emitted

  • ComponentImplemented -- The component, test, and story were generated and verified successfully.
  • ComponentImplementationFailed -- Generation failed with an error message.

Key Concepts

Spec Deltas

The input specification is expressed as four arrays of requirement strings:

  • structure -- DOM structure and component composition requirements
  • rendering -- What the component renders given various prop combinations
  • interaction -- User interaction behavior (clicks, hovers, keyboard)
  • styling -- Visual appearance, Tailwind classes, responsive rules

Scaffold + Body Pattern

When props metadata is provided, the package generates a deterministic scaffold (imports, props type, function signature) with a __BODY__ placeholder. The LLM only fills in the component body, keeping the type-safe skeleton locked down.

Fix Loops

After initial generation, the pipeline runs automated fix loops for:

  • Type errors -- tsc against the component and story files
  • Test failures -- vitest run against the generated test file
  • Lint errors -- biome check with auto-fix, then LLM-assisted fix
  • Story type errors -- tsc against the story file specifically

Each loop has a configurable max iteration count (env vars MAX_TYPE_FIX_ITERATIONS, MAX_TEST_FIX_ITERATIONS, MAX_LINT_FIX_ITERATIONS, MAX_STORY_FIX_ITERATIONS).

Evaluation

A final LLM pass scores the generated output against the original spec deltas, producing a structured EvaluationResult with per-category scores.

Installation

pnpm add @auto-engineer/component-implementor-react

Quick Start

As a message-bus command handler

import { COMMANDS } from '@auto-engineer/component-implementor-react';

// Register the ImplementComponent handler with your message bus
for (const handler of COMMANDS) {
  bus.register(handler);
}

Direct function call

import { handleImplementComponent, type ImplementComponentCommand } from '@auto-engineer/component-implementor-react';

const command: ImplementComponentCommand = {
  type: 'ImplementComponent',
  data: {
    targetDir: './client',
    job: {
      id: 'job_1',
      dependsOn: [],
      target: 'ImplementComponent',
      payload: {
        componentId: 'milestone-badge',
        structure: ['Renders a badge with an icon and label'],
        rendering: ['Shows milestone name and completion percentage'],
        interaction: ['Clicking the badge navigates to milestone details'],
        styling: ['Uses rounded-full with indigo background'],
        storybookPath: 'src/components/MilestoneBadge.stories.tsx',
        files: { create: ['src/components/MilestoneBadge.tsx'] },
        composes: [],
      },
    },
  },
  timestamp: new Date(),
  requestId: 'req-1',
  correlationId: 'corr-1',
};

const result = await handleImplementComponent(command);

if (result.type === 'ComponentImplemented') {
  console.log(`Created ${result.data.componentPath}`);
  console.log(`LLM calls: ${result.data.llmCalls}, fix iterations: ${result.data.fixIterations}`);
  console.log(`Evaluation score: ${result.data.evaluation?.totalScore}`);
}

Using the pipeline agent directly

import { ComponentPipelineAgent, type PipelineInput } from '@auto-engineer/component-implementor-react';
import { InMemorySessionService, Runner } from '@google/adk';

const input: PipelineInput = {
  componentName: 'MilestoneBadge',
  componentPath: '/abs/path/to/MilestoneBadge.tsx',
  testPath: '/abs/path/to/MilestoneBadge.test.tsx',
  storyPath: '/abs/path/to/MilestoneBadge.stories.tsx',
  componentImportPath: '@/components/MilestoneBadge',
  targetDir: '/abs/path/to/client',
  specDeltas: { structure: ['...'], rendering: ['...'], interaction: [], styling: [] },
  projectSection: '',
  composes: [],
  isModify: false,
};

const agent = new ComponentPipelineAgent(model, input);
const runner = new Runner({ appName: 'my-app', agent, sessionService: new InMemorySessionService() });

for await (const event of runner.runEphemeral({
  userId: 'pipeline',
  newMessage: { role: 'user', parts: [{ text: 'Implement component MilestoneBadge' }] },
})) {
  // consume events
}

console.log(agent.metrics); // { llmCalls, fixIterations, evaluation, ... }

How-to Guides

Provide props metadata for deterministic scaffolding

Include a props array in the job payload to get a locked-down scaffold. The LLM only fills the body:

payload: {
  // ...
  props: [
    { name: 'label', type: 'string', required: true, description: 'Badge text', category: 'data' },
    { name: 'variant', type: "'default' | 'success'", required: false, default: "'default'", description: 'Color variant', category: 'visual' },
  ],
}

Provide story variants for deterministic stories

When storyVariants is supplied, stories are scaffolded without an LLM call:

payload: {
  // ...
  storyVariants: [
    { name: 'Default', description: 'Default state', args: { label: 'Sprint 1' } },
    { name: 'Completed', description: 'Completed milestone', args: { label: 'Done', variant: 'success' } },
  ],
}

Modify an existing component

Set the file path under files.modify instead of files.create. The pipeline reads the existing source and instructs the LLM to preserve existing behavior while applying the new spec deltas:

payload: {
  files: { modify: ['src/components/MilestoneBadge.tsx'] },
  // ...
}

Configure fix loop iterations

Set environment variables to control how many times each fix loop retries:

MAX_TYPE_FIX_ITERATIONS=3   # default: 3
MAX_TEST_FIX_ITERATIONS=3   # default: 3
MAX_LINT_FIX_ITERATIONS=2   # default: 2
MAX_STORY_FIX_ITERATIONS=2  # default: 2

Configure the LLM provider

The package uses adk-llm-bridge and reads three environment variables:

CUSTOM_PROVIDER_DEFAULT_MODEL=claude-sonnet-4-20250514
CUSTOM_PROVIDER_BASE_URL=https://api.anthropic.com/v1
CUSTOM_PROVIDER_API_KEY=sk-...

API Reference

Exports

// Command handler for message-bus registration
export const COMMANDS: CommandHandler[];
export const implementComponentHandler: CommandHandler;

// Direct invocation
export function handleImplementComponent(
  command: ImplementComponentCommand,
): Promise<ComponentImplementedEvent | ComponentImplementationFailedEvent>;

// Pipeline agent (Google ADK BaseAgent)
export class ComponentPipelineAgent extends BaseAgent {
  constructor(model: BaseLlm, input: PipelineInput);
  get metrics(): PipelineResult;
}

// Scaffold utilities
export function generateScaffold(input: ScaffoldInput): ScaffoldResult;
export function scaffoldStoryFile(input: ScaffoldStoryInput): string;
export function generatePlaceholderComponent(input: PlaceholderInput): string;
export function generatePlaceholderStory(componentName: string, importPath: string): string;

Types

interface PipelineInput {
  componentName: string;
  componentPath: string;
  testPath: string;
  storyPath: string;
  componentImportPath: string;
  targetDir: string;
  specDeltas: SpecDeltas;
  projectSection: string;
  props?: Array<{ name: string; type: string; required: boolean; default?: string; description: string; category: string }>;
  composes: Array<{ id: string; path: string }>;
  storyVariants?: StoryVariant[];
  existingComponent?: string;
  isModify: boolean;
}

interface PipelineResult {
  success: boolean;
  llmCalls: number;
  fixIterations: number;
  typeFixIterations: number;
  testFixIterations: number;
  lintFixIterations: number;
  storyFixIterations: number;
  evaluation: EvaluationResult | null;
}

interface EvaluationResult {
  totalScore: number;
  categories: Record<string, { score: number; maxScore: number; findings: string[] }>;
}

interface SpecDeltas {
  structure: string[];
  rendering: string[];
  interaction: string[];
  styling: string[];
}

Architecture

The package is built on the Google ADK agent framework. The ComponentPipelineAgent orchestrates a sequence of sub-agents, each responsible for one stage.

flowchart TD
    CMD[ImplementComponent Command] --> PIPELINE[ComponentPipelineAgent]

    subgraph "Stage 1 — Generate (parallel)"
        BODY[ComponentBodyAgent]
        TEST[TestGenerationAgent]
    end

    subgraph "Stage 2 — Story"
        STORY[StoryAgent / scaffoldStoryFile]
    end

    subgraph "Stage 3 — Verify & Fix"
        TC[TypeFixLoopAgent]
        TF[TestFixLoopAgent]
        LF[LintFixLoopAgent]
        SF[StoryFixLoopAgent]
    end

    subgraph "Stage 4 — Score"
        EVAL[EvaluationAgent]
    end

    PIPELINE --> BODY
    PIPELINE --> TEST
    BODY --> STORY
    TEST --> STORY
    STORY --> TC
    TC --> TF
    TF --> LF
    LF --> SF
    SF --> EVAL
    EVAL --> RESULT[ComponentImplemented Event]
Loading

Tool runners

Each fix loop uses synchronous shell tool runners to check the target project:

Runner Command Purpose
type-checker pnpm type-check Run tsc --noEmit on the target project
test-runner npx vitest run --reporter=json Run tests and parse JSON results
lint-runner npx @biomejs/biome check Run Biome lint/format checks
storybook-runner npx storybook test Run Storybook interaction tests

Project context

Before calling the LLM, the pipeline assembles a project context section from the target directory containing:

  • Allowed packages (from package.json dependencies)
  • UI component barrel file exports (src/components/ui/index.ts)
  • Project CSS/Tailwind tokens (src/index.css)
  • Composed component signatures (extracted via TS AST, not full source)