The Vite plugin
windo is a Vite plugin first. It serves the canvas chrome and the preview iframe straight from the dev server, and it teaches Vite where your *.windo.tsx files live so it can hand them to the canvas as one virtual module. Add it to a Vite config, point it at your windos, and the dev server becomes the canvas.
The one import
The plugin ships from a dedicated entrypoint, separate from the runtime API. Import it from @westopp/windo/plugin and call it inside your Vite config's plugins array.
import { defineConfig } from 'vite' import { windo } from '@westopp/windo/plugin' export default defineConfig({ plugins: [windo()], })
That's the whole setup for a default project. Notice there's no @vitejs/plugin-react in the array — windo() returns the React plugin paired with its own, so React refresh and JSX are already wired. Adding @vitejs/plugin-react yourself would register it twice.
What it does at dev time
The plugin runs as an appType: 'custom' server, which means Vite hands HTML routing to the plugin instead of serving an index.html. windo answers two routes itself:
- The canvas chrome at
/— the outer UI with the sidebar, inspector, and toolbar. - The preview iframe at
/__windo/iframe— the isolated document your component actually renders inside.
Both documents are generated in memory and run through Vite's HTML transform, so HMR and module resolution apply to them like any other page. You never author or commit these HTML files; the plugin owns them.
The virtual registry
The link between your *.windo.tsx files and the canvas is a virtual module, virtual:windo-registry. The plugin generates it on demand: it re-exports your resolved windo config and glob-imports every windo file it can find. The canvas imports this one module and gets the full set of windos plus your config, with no manual import list to maintain.
You rarely import it directly — the client does — but knowing it exists explains how a new *.windo.tsx file shows up on the canvas the moment you save it.
Options
All three options are optional. The defaults cover a conventional project; reach for these when your layout differs.
import { defineConfig } from 'vite' import { windo } from '@westopp/windo/plugin' export default defineConfig({ plugins: [ windo({ include: 'src/**/*.windo.tsx', config: 'windo.config.ts', root: process.cwd(), }), ], })
| Name | Type | Description |
|---|---|---|
| include | string | string[] | Glob for your windo files, relative to the project root. Defaults to **/*.windo.tsx. A leading slash is added if missing. If you pass an array, the first entry is used as the pattern. |
| config | string | Path to your windo config file. Relative paths resolve against the root. When omitted, the plugin auto-discovers windo.config.{ts,tsx,mts,js,mjs} in the root. |
| root | string | Base directory for resolving include and config. Defaults to the resolved Vite root (which is process.cwd() unless you set the Vite root yourself). |
One React, not two
A canvas exists to render components from your library — and component libraries that use hooks break the instant two copies of React end up in the same page. The classic symptom is the "invalid hook call" error: the library called useState, but the React it imported isn't the React that owns the render tree.
The plugin closes both doors that let that happen. It adds react and react-dom to Vite's resolve.dedupe, so every import of React across your code, the canvas, and your component library resolves to one copy on disk. And it pre-includes every React entrypoint in optimizeDeps up front:
optimizeDeps: {
include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
}The pre-include matters because the preview iframe pulls in react-dom/client to mount your component. If Vite discovered that entrypoint late — after it had already bundled react — it would re-optimize and could split React across two pre-bundles. Listing them all here fixes React's identity in a single pass, before any component renders. You get one React instance, and hooks just work.
Where next
- The CLI — run the canvas without writing a Vite config at all.
- Writing a windo — author the
*.windo.tsxfiles this plugin discovers.