vite
Build and serve the browser side of your app with Vite, wired to kitrrc.ts by one plugin.
A browser app needs a dev server, a bundler and a build step, and every package you add wants a piece of that setup: a React transform, a Tailwind compiler, a route generator, a list of dependencies to pre-bundle. kitr/vite is one plugin, kitr(), that reads kitrrc.ts and asks each installed package what it needs, so vite.config.ts stays a three-line file however much the app grows.
It is built on Vite. On top it adds the virtual:kitr/providers and virtual:kitr/config modules that app/app.ts imports, the #app, #config, #components, #server and #kitr aliases from the directories in kitrrc.ts, the active platform's port and output directory, and kitr types on every build and on every change to a file a type generator watches.
It runs at build time only; nothing of it ships to the browser. A pure backend never installs it: the server platform runs TypeScript directly. Every browser platform (web, desktop, tauri, mobile) requires it, so kitr add web site brings it in.
The examples on this page build Acme's order screens at app.acme.com.
Install
kitr add viteFirst time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
What kitr add does:
- Adds
@kitr/viteandvitetodevDependenciesand installs them. Pass--no-installto skip the install. - Adds
'@kitr/vite'topackagesinkitrrc.ts. It registers no provider, because nothing runs at app startup. - Writes
vite.config.tswithkitr()as its only plugin, unless one exists. - Adds
vite/clientand@kitr/vite/clienttocompilerOptions.typesintsconfig.json, soimport.meta.env, asset imports and the virtual modules type-check, andvite.config.tstoinclude. - Adds a
## kitr: vitesection toAGENTS.mdsaying thatvite.config.tsholdskitr()and that the plugin wires everything else.
You rarely run kitr add vite yourself: kitr add web site, kitr add desktop app, kitr add tauri app and kitr add mobile phone require it, and every kit with a browser platform includes it.
Configure
kitr/vite has no config file. The file it writes is the whole configuration:
// vite.config.ts
import { kitr } from 'kitr/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [kitr()],
})
kitr() takes options for the cases where the defaults are wrong:
| Option | Default | Description |
|---|---|---|
root |
Vite's root | The project directory holding kitrrc.ts. |
platform |
KITR_PLATFORM, then the first web platform |
Which platform from kitrrc.ts this build is for. kitr dev <name> and kitr build <name> set the variable. |
start |
what the packages ask (router asks for it) |
false keeps the plain TanStack Router plugin without Start; an object passes options to tanstackStart(). |
plugins |
every declared plugin | { tailwind: false } leaves out the plugin a package declares, by its short name. |
router |
{} |
Extra options for @tanstack/router-plugin. |
routesDirectory |
directories.routes |
Where the route files are. |
generatedRouteTree |
<app>/routeTree.gen.ts |
Where the route tree is written. |
devtools |
on, except in production builds | false leaves every development-only provider out of this build too. |
types |
true |
Run kitr types on build start and on changes in dev. |
The platform in kitrrc.ts sets the rest. web({ port: 3000 }) becomes server.port with strictPort, and kitr build site writes dist/site:
// kitrrc.ts
import { defineKitrConfig } from 'kitr/app'
import { web } from 'kitr/web'
export default defineKitrConfig({
packages: ['@kitr/vite', '@kitr/react', '@kitr/router'],
providers: [],
platforms: { site: web({ mode: 'ssr', port: 3000 }) },
})
Build the order screens
With kitr add web site done, the app object imports its providers and config from the virtual modules and the dev server serves it:
// app/app.ts
import { createApp } from 'kitr/app'
import { config } from 'virtual:kitr/config'
import { providers } from 'virtual:kitr/providers'
import { env } from './env'
export const app = createApp({ providers, config, env })
kitr dev siteFirst time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
The browser opens http://localhost:3000 with the routes under app/routes. virtual:kitr/providers is the providers list from kitrrc.ts as static imports, so every provider is in the bundle and tree-shakable, and virtual:kitr/config is every file under config/ keyed by name: config/query.ts becomes config.query. Change kitrrc.ts and both modules reload without a restart.
Ask the build for something from a package
A package tells the plugin what it needs through the vite field of its setup module, so adding the package is the whole setup. @kitr/tailwind declares its plugin and the dependencies to pre-bundle; @kitr/server keeps config/server.ts out of the browser bundle; @kitr/devtools drops its provider from production builds; @kitr/router turns TanStack Start on:
// packages/tailwind/src/kitr.ts (excerpt)
export default definePackage({
name: 'tailwind',
vite: {
plugin: '@kitr/tailwind/vite',
optimizeDeps: ['clsx', 'tailwind-merge', 'class-variance-authority'],
},
})
| Need | What the plugin does |
|---|---|
plugin |
Imports the module from the project and adds its default export's result to plugins. |
optimizeDeps |
Adds the names that package.json installs to optimizeDeps.include. |
serverOnlyConfig |
Leaves config/<name>.ts out of virtual:kitr/config in browser bundles. |
start |
Renders routes through TanStack Start instead of the router plugin alone. |
provider: { production: false } |
Leaves the package's provider out of production builds. |
Your own package does the same: see the package contract.
Pick the platform to build
A project with two browser platforms, say a web site and a desktop shell, builds each with its own port and output directory. kitr dev <name> and kitr build <name> set KITR_PLATFORM, and the plugin reads that platform's options:
// kitrrc.ts (excerpt)
platforms: {
site: web({ mode: 'ssr', port: 3000 }),
app: desktop({ port: 3100 }),
},
kitr dev site # Vite on :3000, dist/site
kitr dev app # Vite on :3100 inside the Electron window, dist/app
kitr build # both, one after the otherFirst time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
Running vite directly picks the first web platform. To build for another one without the CLI, set the variable or the option: KITR_PLATFORM=app vite build, or kitr({ platform: 'app' }) in a second config file.
Import through the aliases
The directories block in kitrrc.ts names where the app lives, and the plugin turns each entry into an alias, so a move of app/ to src/ is one edit in kitrrc.ts rather than a search and replace. tsconfig.json carries the same paths for the editor:
// app/routes/orders.tsx
import { OrderList } from '#components/order-list'
import { ordersQuery } from '#app/features/orders/queries'
import { Orders } from '#kitr/actions'
import type { AppRoutes } from '#server/routes'
| Alias | Directory |
|---|---|
#app |
directories.app, default app |
#config |
directories.config, default config |
#components |
directories.components, default app/components |
#server |
directories.server, default server |
#kitr |
.kitr, the generated types |
Change the port or the base path
The platform options are the source of truth, so change them there rather than in vite.config.ts, where the plugin would overwrite them:
kitr config site port 4000
kitr config site base /orders/First time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
kitr dev site --port 4000 and KITR_SITE_PORT=4000 override the option for one run. Vite's own server.port in vite.config.ts loses to the platform's, because strictPort is set from it; a port taken by another process fails fast instead of drifting to the next free one.
Prerender pages
In static mode every route is prerendered at build time. In ssr mode the seo package's prerender list picks the routes to prerender ahead of time while the rest render per request. The plugin reads config/seo.ts statically, without executing it, and passes the list to TanStack Start:
// config/seo.ts
import { defineConfig } from 'kitr/seo'
export default defineConfig({
site: { url: 'https://app.acme.com' },
prerender: ['/', '/pricing', '/docs'],
})
kitr build site
# dist/site/client/index.html, pricing/index.html, docs/index.html, sitemap.xml
With site.url set, Start writes sitemap.xml from the same list. A route that reads request data at render time stays out of the list and renders on the server.
Pass options to TanStack Start
start takes an object when the defaults are not enough. It is spread last, so anything here wins over what the plugin set:
// vite.config.ts
import { kitr } from 'kitr/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
kitr({
start: { prerender: { enabled: true, crawlLinks: true } },
router: { quoteStyle: 'single', semicolons: false },
}),
],
})
router goes to @tanstack/router-plugin, which generates app/routeTree.gen.ts; start to tanstackStart(). See the TanStack Start configuration for the full list.
Keep generated types current
kitr types writes .kitr/: the platform types from kitrrc.ts, the action names from app/actions/, whatever each package's generator produces. The plugin runs it when a build starts, and in dev it watches the globs the generators declare and reruns only the generator whose files changed:
kitr dev site
# ✔ kitr types: 2 file(s) written (platforms, actions)
# app/actions/orders.ts changed → actions regenerated
// app/features/orders/queries.ts
import { Orders } from '#kitr/actions' // typed from app/actions/orders.ts, always current
export const ordersQuery = Orders.list
Pass types: false to leave it to a kitr types step of your own, for example in a CI job that builds without the CLI installed.
Add a Vite plugin of your own
kitr() returns a list, so it sits next to any other plugin:
// vite.config.ts
import { kitr } from 'kitr/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [kitr(), visualizer({ filename: 'dist/stats.html' })],
})
To replace a plugin a package declares, leave it out by name and add your own: kitr({ plugins: { tailwind: false } }) with tailwindcss() beside it.
Read the build target in app code
import.meta.env.KITR_PLATFORM is the platform name this bundle was built for, KITR_PLATFORM_TYPE its type and KITR_PACKAGES the packages from kitrrc.ts, all typed by @kitr/vite/client:
// app/components/shell.tsx
if (import.meta.env.KITR_PLATFORM_TYPE === 'desktop') {
// the Electron window has its own title bar
}
For runtime questions (online, foreground, the OS) use platform instead; these constants are fixed at build time.
Testing
Vitest runs the same plugin, so tests import virtual:kitr/*, #app/* and route files exactly as the app does. The vitest.config.ts that kitr add testing writes leaves Tailwind out, because tests check classes, not CSS:
// vitest.config.ts
import { kitr } from 'kitr/vite'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [kitr({ plugins: { tailwind: false } })],
test: {
environment: 'happy-dom',
include: ['app/**/*.test.{ts,tsx}', 'test/**/*.test.{ts,tsx}'],
},
})
// test/orders.test.ts
import { app } from '#app/app'
import { expect, it } from 'vitest'
it('wires the providers from kitrrc.ts', async () => {
await app.boot()
expect(app.status).toBe('booted')
})
Next steps
- API reference
- Vite documentation
- The package contract: the
vitefield a package declares - Web platform: modes, PWA and
kitr dev site - Testing:
createTestAppandrenderWithKitr