# ReactAppStarter — Emitter Architecture

**Audience**: anyone touching the view emitter for Factory B (the "starter-kit" React factory).

**Companion docs**:
- `specverse-self/docs/guides/VIEW-RENDERING-ARCHITECTURE.md` (the "one pattern library, three consumers" design)
- `specverse-self/docs/plans/2026-04-17-UNIFIED-VIEW-RENDERING.md` (the migration plan)

---

## The problem this solves

Factory B emits a standalone React codebase. For every view in the inferred spec (dev defaults + user-defined views), it writes a `.tsx` file the user can read, fork, and edit. The generated code:

- Has no `@specverse/runtime` dependency (fully standalone).
- Is idiomatic React with typed props, hooks, event handlers — not a big static HTML blob.
- Renders the **same** visual output as `@specverse/runtime` renders for the same spec (verified by Phase 3 parity test P3).

The tension: "idiomatic React structure" wants hand-crafted JSX. "Same visual output as runtime" wants a single rendering authority. Strategy 3 resolves this by splitting *structure* (hand-written skeleton templates) from *interior* (canonical Tailwind adapter rendering).

## The three pieces

```
┌─────────────────────────────────────────────────────────────┐
│ 1. view-emitter.ts  — orchestrator                          │
│    Takes (viewSpec, modelSpec, expandedSpec) → .tsx string  │
└──────────────┬───────────────────────────────┬──────────────┘
               │                               │
               ▼                               ▼
┌──────────────────────────────┐  ┌─────────────────────────────────┐
│ 2. skeletons/*.tsx.template  │  │ 3. html-to-jsx.ts  — transformer│
│    One per view type:        │  │                                 │
│      list.tsx.template       │  │    Converts Tailwind-HTML       │
│      detail.tsx.template     │  │    strings from the runtime     │
│      form.tsx.template       │  │    adapter into JSX-safe source │
│      dashboard.tsx.template  │  │    (class→className, self-close,│
│      + specialist types      │  │    brace escape, attribute map) │
│                              │  │                                 │
│    Each has a {{BODY}}       │  │    ~50-150 LoC, bounded scope   │
│    placeholder the emitter   │  │                                 │
│    fills with transformed    │  │                                 │
│    JSX.                      │  │                                 │
└──────────────────────────────┘  └─────────────────────────────────┘
                                              ▲
                                              │
                                              │ renders body as HTML
                                              │
                        ┌─────────────────────┴──────────────────────┐
                        │ @specverse/runtime/views/tailwind          │
                        │  createUniversalTailwindAdapter()          │
                        │  (the canonical build-time renderer —      │
                        │   same code app-demo and Factory A         │
                        │   transitively rely on at runtime)         │
                        └────────────────────────────────────────────┘
```

## Data flow for ONE view

Given an inferred `PostListView` in the expanded spec:

1. **view-emitter.ts** receives `(view, model, expandedSpec)`.
2. Picks the correct skeleton based on `view.type`: `skeletons/list.tsx.template`.
3. Builds a render context for the pattern: `{ primaryModel: 'Post', modelSchemas: {...}, viewSpec: view, modelData: [] }` (empty data — structure only).
4. Calls `createUniversalTailwindAdapter()` and renders the composite pattern's interior → HTML string.
   Example output: `<table class="w-full ..."><thead><tr><th class="...">Title</th>...`.
5. Passes that HTML through **html-to-jsx.ts** → JSX-safe source.
   Example: `<table className="w-full ..."><thead><tr><th className="...">{/* data.map */}Title</th>...`.
6. Substitutes `{{BODY}}` in the skeleton with the transformed JSX.
7. Substitutes skeleton placeholders like `{{MODEL_NAME}}`, `{{PLURAL}}`, `{{PROPS_INTERFACE}}` with model-specific values.
8. Returns the complete `.tsx` source string.

The factory's realize pass calls this once per view, writes each to `frontend/src/views/{ViewName}.tsx`, updates `.specverse-gen/hashes.json`.

## Why this split works

- **Idiomatic outer structure** — skeletons are hand-written `.tsx.template` files, tweaked by the humans who maintain the factory. They define imports, props interfaces, hook placement, event wiring, loading states, empty states. Exactly what a human React author would write.
- **Pattern-driven interior** — the actual markup (table header cells, row templates, form fields, badge styling) comes from the canonical Tailwind adapter. When runtime improves the pattern rendering, Factory B inherits the fix automatically — because the adapter is the single source.
- **No full re-implementation** — we don't build a second renderer. The adapter already works for runtime; we reuse it at build time.

## The html-to-jsx transformer — scope

Input: HTML string with Tailwind classes. Single root element (always — the adapter always produces a containing element).

Output: A JSX-safe source string that can be dropped between JSX tags.

What it does:
1. `class="..."` → `className="..."`
2. Self-closing tags: `<img src="...">` → `<img src="..." />`
3. Escape `{` and `}` in text content by wrapping in `{'{'}` / `{'}'}` when they appear outside attribute values.
4. Convert `style="color: red"` to `style={{ color: 'red' }}`.
5. Convert `for="..."` to `htmlFor="..."`.
6. Strip any attributes that aren't valid in JSX.

What it explicitly does NOT do:
- Parse JavaScript inside the HTML (the adapter produces static HTML — no JS expressions).
- Insert React hooks or event handlers — those live in the skeleton, not the interior.
- Handle SVG / MathML nuances — only HTML+Tailwind.

Test coverage target: feed the transformer 50 pre-captured HTML snippets from the canonical adapter output, assert the JSX output parses as valid TSX via `typescript.transpileModule`.

## Skeleton template format

A skeleton is a `.tsx.template` file — syntactically valid TypeScript with substitution markers. Example (sketch for `list.tsx.template`):

```tsx
/**
 * {{MODEL_NAME}}ListView — generated by @specverse/realize (ReactAppStarter)
 *
 * Safe to edit. Edits are preserved across regeneration (content-hashed).
 * Regenerate with `spv realize` or, if this file was edited, delete the
 * file first to opt back into regeneration.
 */
import { useState, useMemo } from 'react';
import { use{{PLURAL_MODEL}}Query, useDelete{{MODEL_NAME}}Mutation } from '../hooks/useApi';
import type { {{MODEL_NAME}} } from '../types/api';

interface {{MODEL_NAME}}ListViewProps {
  onSelect?: (item: {{MODEL_NAME}}) => void;
  onCreate?: () => void;
}

export function {{MODEL_NAME}}ListView({ onSelect, onCreate }: {{MODEL_NAME}}ListViewProps) {
  const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
  const deleteItem = useDelete{{MODEL_NAME}}Mutation();
  const [searchTerm, setSearchTerm] = useState('');

  const filtered = useMemo(
    () => items.filter(item =>
      Object.values(item).some(v => String(v).toLowerCase().includes(searchTerm.toLowerCase()))
    ),
    [items, searchTerm]
  );

  if (isLoading) return <div className="p-4 text-gray-500">Loading…</div>;
  if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;

  return (
    <div className="p-6 space-y-4">
      <div className="flex justify-between items-center">
        <input
          type="search"
          placeholder="Search {{PLURAL_LOWER}}…"
          value={searchTerm}
          onChange={e => setSearchTerm(e.target.value)}
          className="rounded border px-3 py-2 w-64"
        />
        <button
          onClick={onCreate}
          className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
        >
          + New {{MODEL_NAME}}
        </button>
      </div>

      {/* {{BODY}} — pattern-rendered table interior, JSX-transformed */}
      {{BODY}}

      {filtered.length === 0 && (
        <div className="p-8 text-center text-gray-400">No {{PLURAL_LOWER}} yet.</div>
      )}
    </div>
  );
}
```

Substitution contract:
- `{{MODEL_NAME}}` — PascalCase model name (e.g., `Post`).
- `{{PLURAL_MODEL}}` — PascalCase plural (e.g., `Posts`).
- `{{PLURAL_LOWER}}` — lowercase plural (e.g., `posts`).
- `{{BODY}}` — exactly one substitution per skeleton, where the pattern-rendered interior goes.

Substitution is plain string replacement — no template expression evaluation.

## File placement

```
engines/libs/instance-factories/applications/templates/react-starter/
├── README.md                        # this file
├── view-emitter.ts                  # orchestrator (step 1)
├── html-to-jsx.ts                   # transformer (step 3)
├── skeletons/
│   ├── list.tsx.template            # list view
│   ├── detail.tsx.template          # detail view
│   ├── form.tsx.template            # form view (Phase 2e)
│   ├── dashboard.tsx.template       # dashboard (Phase 2e)
│   └── specialist-*.tsx.template    # board/timeline/calendar (Phase 2e)
└── __tests__/
    ├── html-to-jsx.test.ts          # transformer unit tests
    └── view-emitter.test.ts         # end-to-end: (spec, view) → valid TSX
```

## Build order (Phase 2a in the migration plan)

1. Implement `html-to-jsx.ts` first (smallest, most testable, standalone).
2. Write `list.tsx.template` (one view type; production-quality).
3. Implement `view-emitter.ts` — orchestrator that wires html-to-jsx + canonical Tailwind adapter + skeleton.
4. Integration test: take a reference spec with one model + its list view, emit, assert the `.tsx` source parses and its JSX matches the expected shape.
5. Only after list works end-to-end: template out to detail + form + dashboard.

## Open questions (inside Phase 2a)

1. **Where do per-model hooks come from?** `use{{PLURAL_MODEL}}Query` and `useDelete{{MODEL_NAME}}Mutation` in the skeleton presume a generated hooks file (`src/hooks/useApi.ts`) that provides them. `ReactAppRuntime` already generates this file via `use-api-hooks-generator.ts` — Factory B should reuse that generator (shared between both factories as called out in the migration plan).
2. **How does the skeleton address auto-generated fields (id, createdAt, etc.)?** The view should skip them in the table/form. `isAutoGeneratedField()` logic needs to be available in the generated output. Plan: emit a `src/lib/field-helpers.ts` alongside the views (inlined from the pattern library's logic, same way the runtime has it).
3. **What happens when the view spec has `uiComponents` that the pattern library doesn't recognize?** Runtime gracefully falls back to a default table. Factory B should do the same — same fallback logic, same output.
4. **Should the skeleton expose a slot for user-added React nodes (e.g. `<Children />` or a `prepend` prop)?** Useful for extensibility without forcing users to edit the generated file. Defer to v2 of the emitter — start with fixed skeletons.

## Non-goals

- No JSX AST parsing or manipulation. `html-to-jsx.ts` is a string transformer, not a compiler.
- No server-side rendering of the React components. The emitter outputs source files; the user's Vite build bundles them.
- No runtime customization of skeletons. A project that wants different skeletons forks the factory or the generated code.
