---
name: docs-build-pipeline
description: >
  How the Prefab docs build pipeline works: the scripts in
  docs-build/, what they do, and how to update them. Use when
  modifying build scripts, debugging doc generation failures, adding new
  doc source paths, updating CSS safelists, or troubleshooting preview
  rendering.
---

## Overview

`prefab dev build-docs` runs the full pipeline. The scripts live in
`docs-build/` and execute in this order:

1. **build:renderer** (`npm run build:renderer` in `renderer/`) — builds
   `renderer/src/embed.tsx` as an ESM bundle with code splitting via Vite
   library mode. Output: `docs/renderer.js` (tiny entry loader) +
   `docs/_renderer/*.mjs` (lazy-loaded chunks). Config:
   `renderer/vite.config.renderer.ts`.
2. **render_previews.py** — finds `<ComponentPreview auto>` blocks in MDX
   files, executes the Python code, serializes component trees to JSON,
   rebuilds the CodeGroup interior (Python + JSON tabs) and the `json`
   prop on the tag
3. **generate_content.py** — concatenates all rendered MDX into
   `content.html` for Tailwind to scan
4. **Tailwind CSS build** — compiles `input.css` → scoped CSS using the
   content.html and MDX files as sources
5. **scope_css.py** — rewrites the compiled CSS to scope under shadow DOM
   (`:host` selectors)
6. **generate_playground_bundle.py** — builds the playground JSON bundle
7. **extract_examples.py** — extracts Python code blocks from MDX files
   into `renderer/src/playground/examples.json`
8. **generate_protocol_pages.py** — introspects Pydantic models and writes
   one MDX page per component/action into `docs/protocol/`
9. **generate_protocol_ref.py** — updates `## Protocol Reference` sections
   in component MDX files with compact pseudocode JSON blocks from Pydantic schemas

## Source Paths

Scripts read MDX files from `docs/` (components, actions, patterns, etc.)
and write generated output to:
- `docs/renderer.js` — entry loader (see Renderer Architecture below)
- `docs/_renderer/*.mjs` — lazy-loaded renderer chunks
- `docs/protocol/` — autogenerated protocol reference pages
- `docs-build/content.html` — concatenated content for Tailwind
- `renderer/src/playground/examples.json` — playground examples
- `renderer/src/playground/bundle.json` — playground bundle

If the docs directory structure changes, update the path constants in each
script. Key locations:
- `render_previews.py`: `docs_dir = Path(__file__).resolve().parents[1]`
- `generate_protocol_pages.py`: `PROTOCOL_DIR = Path(__file__).resolve().parents[1] / "protocol"`
- `extract_examples.py`: `docs_dir = root / "docs"`
- `input.css`: `@source "../**/*.mdx"` glob

## CSS Safelist

`renderer/src/index.css` has `@source inline(...)` lines that safelist
Tailwind classes available via `cssClass`. When adding new layout utilities
or CSS class patterns to components, extend the appropriate safelist line.

Current safelists cover: gap, grid-cols, items-*, justify-*, place-items-*,
flex utilities, spacing, sizing, typography, colors, and more.

## Common Tasks

**Adding a new component:** After writing the Python model and renderer,
run `prefab dev build-docs` to generate the protocol page in `docs/protocol/`.
Then write the component doc in `docs/components/`.

**Changing doc directory structure:** Update path constants in all scripts
listed under Source Paths above, plus the `@source` glob in `input.css`.

**Debugging blank previews:** Usually means the Python code block in
`<ComponentPreview auto>` raised an error during `render_previews.py`.
Run the script directly to see the traceback:
```bash
uv run docs-build/render_previews.py
```

**Previews render but look wrong in browser:** Check whether the needed
Tailwind classes are safelisted in `renderer/src/index.css`. Dynamic
classes passed via `cssClass` must appear in a safelist to be included
in the compiled CSS.

## Renderer Architecture

The renderer is an ESM bundle with code splitting, built by
`renderer/vite.config.renderer.ts`. The build produces:

```
docs/renderer.js          — tiny entry loader (~60 bytes)
docs/_renderer/embed-*.mjs  — core bundle (React, Radix, shadcn, Tailwind CSS)
docs/_renderer/charts-*.mjs — recharts (lazy, ~648KB)
docs/_renderer/content-*.mjs — highlight.js + react-markdown (lazy, ~375KB)
docs/_renderer/compound-calendar-*.mjs — date-fns (lazy, ~115KB)
```

### How Loading Works

Mintlify auto-injects every `.js` file from the docs directory as a
non-module `<script>` tag on every page. This creates two constraints:

1. The entry file (`renderer.js`) cannot contain ESM `import`/`export`
   statements — they cause SyntaxError in non-module script context.
2. Chunk files must avoid `.js` extension or Mintlify will also inline them.

The solution has two parts:

**Entry rewriting** — The `rewriteEntryLoader` Vite plugin
(`renderer/vite-plugins.ts`) post-processes the entry after Vite builds it.
It replaces the normal ESM entry with a one-liner:
```js
window.__prefabReady = import("/_renderer/embed-HASH.mjs");
```
Dynamic `import()` works in any script context (unlike static `import`),
so when Mintlify inlines this as a `<script>` tag, it kicks off an async
load of the core chunk and stores the promise on `window.__prefabReady`.

**Chunk extensions** — All chunks use `.mjs` extension
(`chunkFileNames: "_renderer/[name]-[hash].mjs"` in rollup config). Mintlify
only auto-inlines `.js` files, so `.mjs` chunks are served as normal static
assets and loaded via dynamic `import()`.

### Component Loading Script

`docs/snippets/component-preview.mdx` contains the loading logic that each
doc page uses. The flow:

1. If `window.__prefab` exists, mount immediately (renderer already loaded)
2. Otherwise, if `window.__prefabReady` doesn't exist yet, inject a
   `<script>` tag for `/renderer.js` (which sets `__prefabReady`)
3. Poll for `__prefabReady`, then `.then()` on it to wait for the core
   chunk to finish loading
4. Once loaded, `window.__prefab.mountPreview` is available and the
   component preview mounts

### Lazy Loading

Heavy dependencies are isolated into separate chunks via dynamic imports in
`renderer/src/components/registry.ts`. The registry uses `React.lazy()`
wrappers for charts, code/markdown, and calendar components. When a page
renders one of these components, React's Suspense boundary (in `embed.tsx`)
triggers the chunk download.

Component file organization for clean chunk boundaries:
- `compound-calendar.tsx` — Calendar + DatePicker (isolates date-fns)
- `image.tsx` — Image component (extracted from content.tsx so
  highlight.js/react-markdown can be fully lazy)
- `charts.tsx` — all chart wrappers (isolates recharts)
- `content.tsx` — Code + Markdown (isolates highlight.js + react-markdown)

### Shadow DOM

All rendering happens inside shadow roots for CSS isolation from the host
page. The Tailwind CSS is inlined as a string via Vite's `?inline` import
and processed through the `tailwindShadowDom` plugin
(`renderer/vite-plugins.ts`) to strip `@property` declarations and rewrite
`:root` → `:host`. This CSS is injected into each shadow root at mount time.

### Chart Rendering

Charts use recharts' `ResponsiveContainer`, which measures its parent on
mount via ResizeObserver. When charts lazy-load inside a Suspense boundary,
the initial measurement can happen before layout settles (reporting 0 width).
`ChartContainer` (`renderer/src/ui/chart.tsx`) works around this by
dispatching a window resize event on `requestAnimationFrame` after mount,
plus `w-full` on the container div to ensure there's a width to measure.

### Build Pipeline (CLI)

`src/prefab_ui/cli/cli.py` handles copying build output to docs:
- Removes stale `docs/renderer*.js` files and `docs/_renderer/` directory
- Copies fresh `renderer/dist/renderer.js` and `renderer/dist/_renderer/`

When rebuilding during development, also copy to the Mintlify cache:
```bash
MINTLIFY_CACHE="$HOME/.mintlify/mint/apps/client/public"
cp docs/renderer.js "$MINTLIFY_CACHE/renderer.js"
rm -rf "$MINTLIFY_CACHE/_renderer"
cp -r docs/_renderer "$MINTLIFY_CACHE/_renderer"
```

### Linting Exclusions

The `.mjs` chunk files are minified third-party code. They're excluded from:
- **prettier** — `exclude: ^(…|docs/_renderer/)` in `.pre-commit-config.yaml`
- **codespell** — `exclude: ^(…|docs/_renderer/.*)$` in `.pre-commit-config.yaml`
