# Live framework recipes

> Copy-paste live config shapes and adapter notes for Vite, Next.js, Nuxt, SvelteKit, TanStack, Astro, multipage, and CSP-aware fixtures under tests/framework-fixtures.

- Repository: pbakaus/impeccable
- GitHub: https://github.com/pbakaus/impeccable
- Human docs: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4
- Complete Markdown: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4/llms-full.txt

## Source Files

- `skill/reference/live-setup.md`
- `skill/scripts/live/frameworks/index.mjs`
- `skill/scripts/live/frameworks/sveltekit.mjs`
- `skill/scripts/live/frameworks/nextjs.mjs`
- `skill/scripts/detect-csp.mjs`
- `tests/framework-fixtures/README.md`
- `tests/live-e2e.test.mjs`

---

---
title: "Live framework recipes"
description: "Copy-paste live config shapes and adapter notes for Vite, Next.js, Nuxt, SvelteKit, TanStack, Astro, multipage, and CSP-aware fixtures under tests/framework-fixtures."
---

Live mode injects a browser client into the HTML (or document shell) your dev server actually serves. The project file is `.impeccable/live/config.json`. On inject, `live-inject.mjs` resolves a framework from `skill/scripts/live/frameworks/` (first match wins), then either inserts a marker-wrapped `<script src>` tag or runs a framework adapter that server-renders the document. Fixtures under `tests/framework-fixtures/` are the canonical config shapes and CSP patch references.

## Config shape

Write config at the path boot reports (default `.impeccable/live/config.json`):

```json
{
  "files": ["<path-or-glob>", "..."],
  "exclude": ["<optional-glob>", "..."],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}
```

<ParamField body="files" type="string[]" required>
Paths or globs for the HTML/shell the browser loads (not always “source”). Project-root-relative, forward slashes.
</ParamField>

<ParamField body="exclude" type="string[]">
Optional globs to skip files that a `files` glob would otherwise include (email templates, demo HTML).
</ParamField>

<ParamField body="insertBefore" type="string">
Anchor string; inject runs immediately before the first match. Prefer an anchor present in every listed file.
</ParamField>

<ParamField body="insertAfter" type="string">
Alternative to `insertBefore`: inject after the matching line.
</ParamField>

<ParamField body="commentSyntax" type="'html' | 'jsx'" required>
`html` → `<!-- … -->`. `jsx` → `{/* … */}` for layouts written as JSX/TSX.
</ParamField>

<ParamField body="cspChecked" type="boolean">
Records that the one-time CSP setup step has run. Absent on first setup; set `true` after the agent asks (even if the user declines the patch).
</ParamField>

<Warning>
Hard-excluded paths (not overridable): `**/node_modules/**` and `**/.git/**`.
</Warning>

**Glob syntax:** `**` any segments (including zero), `*` within a segment, `?` one character.

## Detection and inject kinds

Registry priority (do not reorder without updating inject behavior):

| Priority | `name` | Inject kind | Typical shell |
|---:|---|---|---|
| 1 | `sveltekit` | `adapter` | `src/app.html` (hint); real mount via `+layout.svelte` + generated root component |
| 2 | `nuxt` | `adapter` | `app.vue` (hint); real load via generated `.client.ts` plugin |
| 3 | `tanstack-start` | `adapter` | `src/routes/__root.tsx` + generated `ImpeccableLiveRoot` component |
| 4 | `astro` | `tag` | Root layout `.astro` (`is:inline` on script) |
| 5 | `nextjs` | `tag` | `app/layout.*` or `pages/_document.*` |
| 6 | `vite-generic` | `tag` | `index.html` |
| 7 | `static-html` | `tag` (fallback) | multipage / generator HTML |

**Tag inject** writes a marker-wrapped script into each resolved `files` entry.

**Adapter inject** still uses `files` as a detection/CSP hint, but the live client is loaded by generated or patched framework-owned artifacts. Inject state is journaled in `.impeccable/live/inject-journal.json` so the next inject/remove can heal crash leftovers.

Source traits (preview mode, comment syntax for wrap/accept) resolve by **file extension**, not project name. Svelte (`.svelte`) uses `preview: 'component'` under `node_modules/.impeccable-live/`; Astro (`.astro`) uses `styleMode: 'astro-global-prefixed'` and `injectScriptAttrs: 'is:inline '`.

## Framework recipes

Copy the `config` object into `.impeccable/live/config.json`. Paths match the fixtures; adjust to your tree (`src/app/...`, monorepo app dirs, etc.).

### Vite / SPA shell (React, plain HTML, TanStack Router SPA)

Tag inject into the tracked HTML entry.

```json
{
  "files": ["index.html"],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}
```

| Fixture | Notes |
|---|---|
| `vite-react/` | Baseline shell + `src/App.jsx` |
| `tanstack-router-vite/` | Same shell path; no Start adapter |
| `vite8-https/` | Dev over HTTPS; client still loads from live server |
| `vite8-react-base-path/` | `base: '/app/'` — open the app under `/app/` |
| `vite8-react-csp-meta/` | CSP via `<meta http-equiv>`; not auto-patched |

Nested app at repo root with no Vite config at root:

```json
{
  "files": ["**/index.html"],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}
```

Use a glob when static unit checks resolve from the repo root and E2E resolves from `runtime.appDir` (see `monorepo-nested-vite/`). Literal `"index.html"` only works when cwd is already the app root.

### Next.js

Tag inject into the root layout (App Router) or document (Pages). `commentSyntax` is `jsx`.

<Tabs>
<Tab title="App Router">

```json
{
  "files": ["app/layout.tsx"],
  "insertBefore": "</body>",
  "commentSyntax": "jsx",
  "cspChecked": true
}
```

Also valid: `app/layout.jsx`, `src/app/layout.tsx`, etc. Fixtures: `nextjs-app/`, `nextjs-app-router/`.

</Tab>
<Tab title="Pages Router">

```json
{
  "files": ["pages/_document.tsx"],
  "insertBefore": "</body>",
  "commentSyntax": "jsx",
  "cspChecked": true
}
```

</Tab>
<Tab title="Monorepo app">

```json
{
  "files": ["apps/web/app/layout.tsx"],
  "insertBefore": "</body>",
  "commentSyntax": "jsx",
  "cspChecked": true
}
```

Fixture: `nextjs-turborepo/`. Patch CSP on the **app’s** `next.config.*`, not only the shared helper.

</Tab>
</Tabs>

Next has no dedicated inject adapter: the root layout already owns `<html>` / `<body>`, so the generic tag strategy is enough.

### Nuxt

Config lists `app.vue` (or your app-dir equivalent) as the shell hint. A raw `<script>` in `app.vue` is compiled as Vue DOM and **does not execute**. The `nuxt` adapter creates a marked dev-only client plugin, e.g. `plugins/impeccable-live.client.ts` or `app/plugins/impeccable-live.client.ts` when `srcDir` / Nuxt 4 `app/` layout applies, and removes it on stop.

```json
{
  "files": ["app.vue"],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}
```

| Fixture | Notes |
|---|---|
| `nuxt-vite7/` | Static inject/wrap checks; documents that script-in-`app.vue` is wrong |
| `nuxt-csp/` | `routeRules` CSP string → `append-string` patch |

If `plugins/impeccable-live.client.ts` already exists and lacks the Impeccable marker, inject returns `nuxt_plugin_conflict`.

### SvelteKit

Config lists `src/app.html`. The adapter patches the root layout and creates `src/lib/impeccable/ImpeccableLiveRoot.svelte` so the client runs under SSR. Variants use **component preview** (not in-route markup HMR).

```json
{
  "files": ["src/app.html"],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}
```

| Fixture | Notes |
|---|---|
| `sveltekit/` | Shell + route wrap → `node_modules/.impeccable-live/...` preview |
| `vite8-sveltekit/` / `vite8-sveltekit-stateful/` | Runtime E2E; stateful list needs collection props across preview boundary |
| `sveltekit-csp/` | `kit.csp.directives` → `append-arrays` |

Preview modules must live under `node_modules/.impeccable-live` so Vite’s `server.fs.allow` can serve them (SvelteKit restricts FS to `src`, `.svelte-kit`, and `node_modules`).

### TanStack

<Tabs>
<Tab title="Router SPA (Vite)">

Same as Vite shell — no Start adapter:

```json
{
  "files": ["index.html"],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}
```

Fixture: `tanstack-router-vite/`. Multi-route heroes often need `preActions` in E2E (navigate before pick).

</Tab>
<Tab title="Start (SSR)">

No static `index.html`. Inject targets the root document and mounts a generated React root component:

```json
{
  "files": ["src/routes/__root.tsx"],
  "insertBefore": "<Scripts",
  "commentSyntax": "jsx",
  "cspChecked": true
}
```

Fixture: `tanstack-start/`. Detection requires the Start project shape (`detectTanStackStartProject`); plain Router SPA stays on `vite-generic`.

</Tab>
</Tabs>

### Astro

Tag inject into the root layout. Scripts get `is:inline` so Astro does not rewrite `src`. Preview CSS uses global-prefixed rules instead of `@scope`.

```json
{
  "files": ["src/layouts/Layout.astro"],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}
```

Fixtures: `astro/`, `astro-vite7/`. Point `files` at your actual root layout path.

### Multipage and generators

Serve HTML under a build output (or `public/`) with a glob so new pages are covered:

```json
{
  "files": ["dist/**/*.html"],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}
```

Fixture `multipage-with-generator/`:

```json
{
  "files": ["dist/index.html", "dist/docs/one.html"],
  "insertBefore": "</body>",
  "commentSyntax": "html"
}
```

`dist/**` is treated as **generated**. Wrap refuses to edit those files (`element_not_in_source`); accept still lands true source via the fallback flow. Inject into generated HTML only survives until the next rebuild — re-run inject after each build if you rely on the shell tag.

### Quick matrix

| Framework | `files` example | `insertBefore` | `commentSyntax` | Inject |
|---|---|---|---|---|
| Vite / SPA | `["index.html"]` | `</body>` | `html` | tag |
| Next App Router | `["app/layout.tsx"]` | `</body>` | `jsx` | tag |
| Next Pages | `["pages/_document.tsx"]` | `</body>` | `jsx` | tag |
| Nuxt | `["app.vue"]` | `</body>` | `html` | adapter (plugin) |
| SvelteKit | `["src/app.html"]` | `</body>` | `html` | adapter (layout + root) |
| TanStack Router SPA | `["index.html"]` | `</body>` | `html` | tag |
| TanStack Start | `["src/routes/__root.tsx"]` | `<Scripts` | `jsx` | adapter |
| Astro | `["src/layouts/Layout.astro"]` | `</body>` | `html` | tag + `is:inline` |
| Multipage | `["public/**/*.html"]` or `["dist/**/*.html"]` | `</body>` | `html` | tag |

## CSP recipes

First-time setup runs when `cspChecked` is missing:

```bash
node <scripts_path>/detect-csp.mjs
```

Stdout: `{ "shape": "...", "signals": ["..."] }`. Shape is the **patch mechanism**, not the framework name. Priority: `append-arrays` > `append-string` > `middleware` > `meta-tag` > `null`.

| Shape | Auto-patch | Typical sources | Fixture |
|---|---|---|---|
| `null` | none | no CSP | most plain fixtures |
| `append-arrays` | yes (with consent) | Next monorepo `additionalScriptSrc` / `additionalConnectSrc`; SvelteKit `kit.csp.directives`; nuxt-security | `nextjs-turborepo/`, `sveltekit-csp/` |
| `append-string` | yes (with consent) | Next `headers()` CSP string; Nuxt `routeRules` CSP header | `nextjs-inline-csp/`, `nuxt-csp/` |
| `middleware` | no | `middleware.ts` `headers.set('Content-Security-Policy', …)` | (detect only) |
| `meta-tag` | no | `<meta http-equiv="Content-Security-Policy">` | `vite8-react-csp-meta/` |

Live needs `http://localhost:8400` on both **`script-src`** and **`connect-src`**.

### append-arrays

Declare once, spread into directive arrays (dev-only):

```ts
const __impeccableLiveDev =
  process.env.NODE_ENV === "development" ? ["http://localhost:8400"] : [];
```

- **Next monorepo helper:** edit the app’s `next.config.*` and append to `additionalScriptSrc` / `additionalConnectSrc` (reference: `tests/framework-fixtures/nextjs-turborepo/expected-after-patch.ts`).
- **SvelteKit:** `svelte.config.js` → `kit.csp.directives['script-src']` and `['connect-src']` with `...__impeccableLiveDev` (reference: `sveltekit-csp/expected-after-patch.js`).
- Idempotent if `__impeccableLiveDev` already exists: mark `cspChecked: true` without rewriting.

### append-string

Declare a leading-space token and interpolate into both directives:

```ts
const __impeccableLiveDev =
  process.env.NODE_ENV === "development" ? " http://localhost:8400" : "";
```

- Next inline `headers()` in `next.config.*` → `nextjs-inline-csp/expected-after-patch.js`
- Nuxt `routeRules['/**'].headers['Content-Security-Policy']` → `nuxt-csp/expected-after-patch.ts`

### middleware / meta-tag

Show detected paths; ask the user to allow `http://localhost:8400` on `script-src` and `connect-src` manually; then set `cspChecked: true`. Do not invent an auto-edit.

### Consent and re-ask

On decline: skip the patch, warn that live will fail until CSP allows the origin, still set `cspChecked: true`. To re-open the prompt, delete `cspChecked` from config and re-run live boot.

## Config drift

On boot, HTML under common roots (`public/`, `src/`, `app/`, `pages/`) not covered by the resolved `files` list appears as `configDrift.orphans`. Offer once per session to add paths or switch to a glob. Never auto-write the config.

## Fixture catalog (recipes-relevant)

| Fixture | Role |
|---|---|
| `vite-react/`, many `vite8-react-*` | SPA shell + styling variants (Tailwind, CSS modules, Emotion, …) |
| `nextjs-app/`, `nextjs-app-router/` | App Router inject + wrap |
| `nextjs-turborepo/`, `nextjs-inline-csp/` | CSP `append-arrays` / `append-string` |
| `sveltekit/`, `vite8-sveltekit*`, `sveltekit-csp/` | Adapter, component preview, CSP arrays |
| `nuxt-vite7/`, `nuxt-csp/` | Plugin inject shape + CSP string |
| `tanstack-router-vite/`, `tanstack-start/` | Tag SPA vs Start adapter |
| `astro/`, `astro-vite7/` | Layout inject + Astro script attrs |
| `multipage-with-generator/` | Generated HTML + `element_not_in_source` |
| `monorepo-nested-vite/` | `appDir: "website"`, roots resolution |
| `vite8-react-csp-meta/` | Manual CSP meta shape |
| `vite8-https/`, `vite8-react-base-path/` | TLS and base path edge cases |

Layout of each fixture:

```text
tests/framework-fixtures/<name>/
  files/           # staged project tree
  fixture.json     # config + wrap/CSP/runtime expectations
  gitignore.txt    # becomes .gitignore in tmp
  expected-after-patch.*   # CSP references (optional)
```

Runtime E2E (`bun run test:live-e2e`) only runs fixtures that declare `runtime`. Scope with `IMPECCABLE_E2E_ONLY=<fixture>`.

## Troubleshooting

| Symptom | Check |
|---|---|
| Picker never loads | CSP blocks `localhost:8400`; clear `cspChecked` and re-run CSP step |
| Script in HTML but no `window.__IMPECCABLE_LIVE_INIT__` | Wrong framework path: Nuxt/SvelteKit/TanStack Start need adapters, not only a shell tag |
| `file_not_found` on inject | `files` wrong relative to app root; prefer globs in monorepos |
| Wrap errors `element_not_in_source` | Picked DOM only exists in generated HTML; edit true source or accept fallback |
| Inject vanishes after rebuild | Multipage generator output recreated; re-inject after build |
| `nuxt_plugin_conflict` | User-owned file at the plugin path; rename/remove or pick another plugin dir |
| Nested app not detected | Boot from repo root so roots resolution finds `appDir`; set config under the app when E2E uses `runtime.appDir` |

## Next

<CardGroup>
<Card title="Live browser iteration" href="/live-mode">
Start live mode, poll contract, generate/accept/discard, roots, carbonize.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Full `.impeccable/config.json` and live `config.json` field list.
</Card>
<Card title="Build and test" href="/build-and-test">
`test:live-e2e` triggers and framework-fixture obligations.
</Card>
<Card title="Modes and platform" href="/modes-and-platform">
Live is web-only; native platforms skip live and detect overlays.
</Card>
</CardGroup>
