Point0by 1gr14
  • Меню
    • GitHub
    • Блог
  • Introduction
    • Overview
    • Getting Started
    • Full Overview
    • Benchmarks
    • Points
  • Points
    • Page
    • Layout
    • Component
    • Provider
    • Mountable
    • Query
    • Infinite Query
    • Mutation
    • Action
    • Root
    • Base
    • Plugin
  • Methods
    • Validation
    • Loader
    • Context
    • Middleware
    • Loading & error
    • .with
    • Mapper
    • Transformer
    • Stage Methods
  • Core
    • Navigation
    • SSR
    • RSC
    • Request
    • Response
    • Error handling
    • Env
    • Head
    • MDX
    • Assets
    • File upload
    • OpenAPI
    • Query client
    • Events
    • Infer
  • Engine
    • Engine Config
    • Engine Runtime
    • CLI
    • Dev
    • Build
    • Compiler
    • Generator
    • Project MCP
    • Docs MCP
    • Importer
    • Public dir
    • Testing
    • Deploy
    • Bun or Vite
  • Extra
    • SsrStore
    • CookieStore
    • Basic Auth
    • CORS
    • Cache-Control
    • Compress
  • Examples
    • Basic
    • Vite
    • Better Auth
    • Capacitor
    • Expo
  • 1gr14
    • Главная
    • Start0
    • Поддержать
    • Обучение
    • Группа
    • Блог
    • Автор
  • Сообщество
    • Discord
    • Telegram
  • Опенсорс
    • Point0
    • Route0
    • Error0
    • Flat
  • Аккаунт
    • Войти
    • Регистрация
Point0by 1gr14
Создаю опенсорс во славу Господа Иисуса Христа ☦️
С любовью ко всем разработчикам на свете ❤️
Условия использованияПолитика конфиденциальностиСергей Дмитриев 2026 😎

Bun or Vite

  • Раздел: Engine

On this page

  1. Bun is the default
  2. Babel runs under Bun too
  3. What differs between the two paths
  4. bunPlugins ↔ viteConfig.plugins
  5. The dev and build architecture
  6. How to switch from Bun to Vite
  7. Gotchas
  8. Reference
  9. The viteConfig option
  10. Bundler-specific config keys

Point0 ships two bundler paths. By default it runs on pure Bun — no Vite involved. Add a viteConfig to the engine and it switches to Vite for both dev and build. Nothing else in your app changes: the same points, the same index.server.ts and app.client.tsx, the same CLI.

The snippets below are from examples/basic and examples/vite, which use a single client key and index.html. (The production boilerplate uses a clients: [] array and index.client.html instead — same mechanics, different names.)

// examples/basic — pure Bun: no viteConfig anywhere
export const engine = Engine.create({
  file: import.meta.url,
  ssr: true,
  client: {
    bunPlugins: ['bun-plugin-tailwind'], // bundler plugins are Bun plugins
    // ...
  },
})
// examples/vite — same app, with Vite: one extra key flips the bundler
export const engine = Engine.create({
  file: import.meta.url,
  ssr: true,
  viteConfig: ({ plugins, side }) => ({
    plugins: [...plugins, react(/* ... */), tailwindcss()],
  }),
  client: {
    // no bunPlugins here — tailwind moves into viteConfig.plugins
    // ...
  },
})

The presence of viteConfig is the switch — there is no useVite or mode: 'bun' | 'vite' flag.

Bun is the default

Both examples/basic and the production boilerplate ship pure Bun. Bun is the recommended path — nothing extra to install, dev and HMR work out of the box. Vite is an opt-in for projects that already lean on the Vite plugin ecosystem.

Vite is a devDependency of @point0/engine, not a production peer dependency, and Point0 imports it lazily (await import('vite')) only on the Vite path. A Bun-only app never loads Vite at all — the Bun server build even force-externalizes vite and esbuild, so a pure-Bun app builds and runs with neither installed.

Babel runs under Bun too

A Bun-only Point0 app is not "just Bun.build": the Point0 compiler runs Babel inside its own transform, the same way under both bundlers. Bun's native bundler has no Babel stage of its own — Point0 supplies one.

// works identically on Bun and on Vite — the compiler runs babel internally
export const engine = Engine.create({
  client: {
    compiler: { babel: ['babel-plugin-react-compiler'] },
    // ...
  },
})

The compiler ships in three plugin formats — Bun, Vite, and Babel — over one shared codebase, so your compiler.babel plugins are threaded into the same transform under either bundler. babel-plugin-react-compiler is the common case; both example apps enable it. See compiler for the full transform and point0 compile for inspecting the output.

What differs between the two paths

Most of the engine config is identical across the two paths; only a handful of keys are bundler-specific.

ConcernBun pathVite path
Switch key(no viteConfig)viteConfig
Bundler pluginsbunPlugins (Bun plugins)viteConfig.plugins (Vite plugins)
Build tuningbunBuildConfigviteConfig (build.rolldownOptions, …)
Tailwindbun-plugin-tailwind in bunPlugins@tailwindcss/vite in viteConfig.plugins
preloadpreventLoadBunPlugins offpreventLoadBunPlugins: true
Server reload in devimport-graph restart; --hot for point hot-swapVite HMR (re-runs the entry); --hot is a no-op

bunBuildConfig has no effect under Vite, and viteConfig has no effect under Bun.

bunPlugins ↔ viteConfig.plugins

Under Bun, bundler plugins are Bun plugins passed as string names (the native dev server resolves them by name in a generated bunfig.toml):

client: {
  bunPlugins: ['bun-plugin-tailwind'], // for the native dev server, only string entries work (object/function plugins are accepted by the type but error in dev — see Gotchas)
}

Under Vite, they are Vite plugins inside the viteConfig callback. The callback receives plugins — Point0's own compiler Vite plugin is already in that array — and you spread it in wherever you want, then add your own:

viteConfig: ({ plugins, side }) => ({
  resolve: { tsconfigPaths: true },
  plugins: [
    ...plugins, // point0 compiler vite plugin is already here
    react({ include: /\.(jsx|js|mdx|md|tsx|ts)$/ }),
    tailwindcss(),
    side === 'client'
      ? analyzer({ analyzerMode: 'static', openAnalyzer: false })
      : null,
  ],
})

Everything else Vite needs — root, define, build.rolldownOptions — is injected automatically from the rest of your engine config; the callback only adds what you want on top. The callback gets { command, side, mode, scope, plugins }, so you can branch per side (the example above only runs the bundle analyzer on the client).

The dev and build architecture

The two paths run different machinery underneath, though the CLI is the same.

Client dev server. Bun spawns a child process that serves index.html as a Bun HTMLBundle via Bun.serve. Vite runs a real Vite dev server wrapped by a thin Bun.serve that calls transformIndexHtml and lets Vite own HMR.

Server in dev. On the Bun path the server runs as a plain bun run subprocess that Point0's own file/import-graph watcher restarts on change (SIGKILL + respawn) — it is not bun --watch. On the Vite path the server runs in-process through Vite's SSR dev server, which re-runs the entry on change.

Build. Bun builds the client with Bun.build (target browser) and the server with Bun.build (target bun). Vite builds both with vite build (SSR mode for the server). Either way the output layout is the same — dist/client plus dist/server — but the server entry filename differs: Bun emits dist/server/index.server.js (named by source basename), while Vite emits dist/server/main.js (rollup names by the entry key). Use each example's start script rather than hardcoding one path. See build and deploy.

How to switch from Bun to Vite

Three files change. The production boilerplate keeps the Vite block commented out right next to the Bun config, with the spots to edit marked.

1. Add viteConfig to the engine and move bundler plugins into it. Drop client.bunPlugins; add the Vite equivalents (@tailwindcss/vite for tailwind, @vitejs/plugin-react for React Fast Refresh):

import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'

viteConfig: ({ plugins }) => {
  // tailwind packages must be externalized under Vite
  const external = ['bun', '@tailwindcss/vite', '@tailwindcss/oxide', '@tailwindcss/node', 'tailwindcss']
  return {
    plugins: [...plugins, react(/* ... */), tailwindcss()],
    optimizeDeps: { exclude: external },
    ssr: { external },
  }
},

2. Set preventLoadBunPlugins in preload. Under Vite the Bun plugins must not load — Vite provides the transform through its own plugin:

// preload.ts
// Bun:
await engine.preload({ nodeEnvFallback: 'development' })
// Vite:
await engine.preload({
  nodeEnvFallback: 'development',
  preventLoadBunPlugins: true,
})
// Bundler-agnostic form (works either way):
await engine.preload({
  nodeEnvFallback: 'development',
  preventLoadBunPlugins: !!engine.server.viteConfig,
})

3. Adjust index.html. Bun resolves a relative script path and picks up CSS imported from JS. Vite wants an absolute script path and an explicit stylesheet link:

<!-- Bun -->
<script type="module" src="./index.client.tsx"></script>

<!-- Vite: absolute path, and add the stylesheet link in <head> -->
<link rel="stylesheet" href="/styles/index.css" />
<script type="module" src="/index.client.tsx"></script>

The runtime files — index.server.ts, index.client.tsx, app.client.tsx — stay byte-for-byte identical: the bundler swap touches only wiring files.

Gotchas

  • Vite already hot-reloads the server; --hot is a Bun-only extra. Under Vite the server runs in-process and Vite's HMR re-runs the entry on every save — no flag needed. --hot adds a Bun-native point hot-swap (swap an edited point's module without restarting the process), so on the Vite path the flag is a no-op. Both examples ship point0 dev --hot. See dev.
  • Bun dev plugins must be string entries. The native dev server resolves them by name in a generated bunfig.toml; function or object plugins error there. Use viteConfig.plugins for non-string plugins.
  • Tailwind may need externalizing under Vite. The boilerplate externalizes the tailwind packages (optimizeDeps.exclude + ssr.external, the snippet above); the minimal examples/vite app omits it. Add them if the tailwind packages fail to resolve or get pre-bundled under Vite.
  • Don't forget index.html when switching. A wrong script path or a missing stylesheet link is the usual "blank page after switching" cause.
  • vite.config.ts is not used by the engine. When present, it's a thin view for external tooling (vitest, IDE Vite integration) that calls engine.getViteConfig(env). engine.dev() / engine.build() read viteConfig from the engine directly and never load that file.
  • devWatchGlob is almost never needed. In dev the server watcher already walks the entry's deep-import graph and restarts on any change in it — you don't list your files. server.devWatchGlob only adds globs for files the import walk can't see, so most apps leave it unset. The examples/vite app sets ['**/*.{ts,tsx,mdx}', '!generated/point0/meta.ts'] mainly to exclude a generated file from the watch and avoid a regen→restart loop — a niche tweak, not a switching step.
  • Dev server source maps are handled per path; both remap to your source. Bun does not apply source maps to runtime stack traces itself, so on the Bun path Point0 installs source-map-support in dev to remap error stacks back to the original .ts/.tsx. On the Vite path it doesn't — Vite remaps SSR stack traces with its own ssrFixStacktrace. The install is dev-only and a no-op once built.

Reference

The viteConfig option

viteConfig accepts three forms, on the general options and per side (server.viteConfig / client.viteConfig, each falling back to the general one):

FormTypeUse
Callback(opts) => UserConfig | Promise<UserConfig>the common case — receives plugins
Objecta vite UserConfiga static config
Stringa path to a vite.config.tspoint at an external config file

The callback's argument carries { command: 'serve' \| 'build', side: 'client' \| 'server', mode, scope, plugins }, where plugins already includes Point0's compiler Vite plugin.

Bundler-specific config keys

KeyPathLives on
viteConfigVitegeneral, server, client
bunPluginsBungeneral, server, client
bunBuildConfigBungeneral, server, client
devWatchGlobbothserver (extra dev-watch globs, on top of the auto-detected import graph)
compiler.babelbothclient / server compiler

Full engine option coverage is on engine-config; the dev and build internals are on dev and build; the compiler and its Babel stage on compiler.

НазадDeployДалееSsrStore

Enjoying Point0?

Give it a star on GitHub, ask questions, and follow what's new

Star Point0

A star on GitHub helps more developers find Point0
Star on GitHub

Start0

Point0 integrated into the Start0 SaaS boilerplate — the best way to use it
Explore Start0

YouTube

Tutorials, walkthroughs, and updates
YouTube

Discord

Questions and chat in English
Join Discord

Telegram

News channel and chat in Russian
ChannelChat

Twitter

Posts and updates
Twitter