URL: /architecture/vite-plugin

---
title: Vite plugin
description: Virtual modules, HMR, MDX preprocessing, and static-asset middleware.
icon: "zap"
---

# Vite plugin

`packages/tangly/src/plugin/vite-plugin.ts` is the bridge between user state (the manifest, theme, config) and the synthesized Astro runtime.

## Module graph

```mermaid
flowchart LR
  subgraph user[User project]
    U1[docs.json]
    U2["*.mdx"]
    U3[images/, logo/, public/]
  end

  subgraph plugin[Vite plugin]
    P1[buildManifest]
    P2[chokidar watcher]
    P3[transform hook<br/>MDX preprocess]
    P4[middleware<br/>static assets]
    P5[component-shadow<br/>aliases]
  end

  subgraph virt[Virtual modules]
    V1[virtual:tangly/manifest]
    V2[virtual:tangly/routes]
    V3[virtual:tangly/config]
    V4[virtual:tangly/theme]
  end

  subgraph astro[Astro runtime]
    A1["[...slug].astro"]
    A2[Layout / TopNav / Sidebar]
  end

  U1 --> P1
  U2 --> P1
  P1 --> V1
  P1 --> V2
  P1 --> V3
  P1 --> V4
  P2 -. invalidate on change .-> P1
  U2 --> P3 --> A1
  U3 --> P4
  P5 -. user theme/ overrides .-> A2
  V1 --> A1
  V1 --> A2
  V4 --> A2
```

## Virtual modules

Four virtual imports are exposed to the runtime:

| ID                        | Exports                                                                             |
| ------------------------- | ----------------------------------------------------------------------------------- |
| `virtual:tangly/manifest` | The full `Manifest` (config, pages, navigation, orphans, warnings, collections)     |
| `virtual:tangly/routes`   | `[{ slug, file }]` for each page (drafts excluded in build mode)                    |
| `virtual:tangly/config`   | The parsed `DocsJson`                                                               |
| `virtual:tangly/theme`    | `{ themeName, userRoot }` — used by Layout to pick the active theme's CSS           |

Any `.astro` file in the runtime can `import { manifest } from "virtual:tangly/manifest"` and get a typed snapshot.

## Hot reload

A chokidar watcher listens for changes in:

- `<userRoot>/docs.json`
- `<userRoot>/**/*.mdx`
- `<userRoot>/**/*.md`
- `<userRoot>/tangly.config.ts`
- `<userRoot>/_section.mdx` and `<userRoot>/**/_meta.json`

On change, the manifest is invalidated, virtual modules are re-evaluated, and Vite is told to do a full reload via `server.ws.send({ type: 'full-reload' })`. Astro's MDX HMR handles per-page MDX changes natively for sub-page edits.

## Static-asset middleware

Mintlify projects use absolute paths for images: `<img src="/images/foo.png">`. Tangly's middleware resolves these against the user root's matching directory:

```
GET /images/foo.png  →  <userRoot>/images/foo.png
GET /logo/dark.svg   →  <userRoot>/logo/dark.svg
GET /favicon.ico     →  <userRoot>/<config.favicon>
```

Supported prefixes: `images`, `logo`, `public`, `static`, `assets`. Path traversal is blocked — requests like `/images/../../etc/passwd` are rejected before any I/O.

## Build-time variant

In production, `packages/tangly/src/build-outputs/copy-assets.ts` walks the same set of directories and copies them into `dist/` so deployed pages see the same paths the dev server served.

## MDX preprocess

The plugin's `transform()` hook rewrites Mintlify-only quirks before MDX parses JSX:

- `<latex>...</latex>` → `$$...$$` block math (so curly-brace LaTeX doesn't trigger MDX expression parsing).
- Relative Markdown image refs (`![](../images/foo)`) → absolute paths (`/images/foo`) so Astro's asset pipeline doesn't mis-resolve them in nested routes.

More compatibility shims land here as we hit them.

## Component shadowing

`packages/tangly/src/plugin/component-shadow.ts` rewrites Vite resolution for `@tanglydocs/theme-ui/<Name>.astro`:

1. `<userRoot>/theme/<Name>.{astro,tsx,jsx}` (project-level override)
2. `@tanglydocs/theme-<active>/components/<Name>.astro` (theme-level override)
3. `@tanglydocs/theme-ui/components/<Name>.astro` (built-in default)

The lookup runs at module-resolution time, not import time, so overrides have zero runtime cost — Vite produces the same code as if you'd written the import directly.
