Vite plugin
Virtual modules, HMR, MDX preprocessing, and static-asset middleware.
~ 2 min read
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
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.mdxand<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 (
) → absolute paths (/images/foo) so Astro’s asset pipeline doesn’t mis-resolve them in nested routes.
Both replacements run on raw MDX text but skip matches inside code spans and fenced code blocks (via replace-outside-code.ts), so docs describing these shims can quote the literal trigger patterns in backticks (this very paragraph does) without being silently rewritten.
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:
<userRoot>/theme/<Name>.{astro,tsx,jsx}(project-level override)@tanglydocs/theme-<active>/components/<Name>.astro(theme-level override)@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.