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 😎

Build

  • Раздел: Engine

On this page

  1. Generate runs first, always
  2. Server and client build in parallel
  3. Bun or Vite, per side
  4. The build is production by default
  5. Running the build
  6. Source maps
  7. Module preload manifest
  8. Watch mode
  9. Partial builds
  10. Clean and publicdir
  11. Reference
  12. point0 build flags
  13. engine.build()
  14. Per-side build config

point0 build turns your app into a dist/ you can run in production. It always generates first, then bundles the server and every client in parallel — each side with Bun (the default) or Vite. The server bundle runs your app and server-renders each first page load (when SSR is on); the client bundle is the static browser code it serves — the one that hydrates that render and then drives client-side navigation between pages (SPA-style).

point0 build
# Build...
# Build completed in 1843ms

That command is a thin wrapper over engine.build() (see CLI). The result is two trees under your configured outdirs:

dist/
  server/index.server.js   # the server entry — `bun run` this in prod
  client/index.html         # the HTML shell (server-rendered into on first load)
  client/**.js              # client chunks (lazy page chunks, shared chunks)
  client/_point0/assets/…    # hashed asset bytes, served at /_point0/assets/<hash>
  client/_point0/<scope>/preload-manifest.json   # per-page module preload manifest
  client/_point0/<scope>/build-version.json  # the build identity ([deploy](deploy) invalidation)
  client/_point0/<scope>/build-assets.json   # content-hashed file list (server-only; asset caching)

Generate runs first, always

There is no "build without generate". engine.build() calls the generator before it bundles anything:

// inside engine.build()
await this.generator.sync({ logOnNotWritten: true }) // codegen first
// ...then clean + bundle server and clients

This is on purpose: a stale points aggregator means wrong or missing pages, and an empty in-memory point set means no per-page preload. Generate emits the files your app imports — the points aggregators (points.server.ts / points.client.ts), the routes table, the analyzer meta.ts, and the asset *.d.ts — all under src/generated/point0/ (gitignored). Wiring lives in the engine config:

// examples/basic/src/engine.ts
generate: { meta: './generated/point0/meta.ts', assetsTypes: './generated/point0/assets.d.ts' },
server: { generate: { points: './generated/point0/points.server.ts' } },
client: { generate: {
  points: './generated/point0/points.client.ts',
  routes: { outfile: './generated/point0/routes.ts', origin: 'process.env.CLIENT_URL' },
} },

You can run codegen on its own with point0 generate — full details on Generator.

Server and client build in parallel

After generate, the two sides bundle at the same time:

await Promise.all([buildClientsPromise, buildServerPromise])

All clients build in parallel too, so a multi-client app doesn't pay for each one in series. The whole run is timed and logged as Build completed in {ms}ms.

The two sides differ in target and intent:

Server bundleClient bundle
Targetbun (or ssr: true under Vite)browser
FormatESMESM, code-split (splitting: true)
Minifiedalwaysonly when NODE_ENV=production
Served to users?no — you run ityes — the server serves dist/client at /
Entryyour entry files + the engine filethe indexHtml

The server is never sent over the wire, so it's always minified. The client bundle is public — treat it as code anyone can read: server-only code is stripped by the compiler at build time, not guarded at runtime.

Bun or Vite, per side

By default each side builds with Bun (Bun.build), with no config needed — examples/basic, the canonical Bun app, sets no build config at all. To reach into the Bun build, pass bunBuildConfig on that side: a plain Partial<Bun.BuildConfig> (or ({ mode, side }) => …) spread over Point0's defaults, so you can override any default — target, minify, define, external, naming, plugins, anything Bun.build accepts:

export const engine = Engine.create({
  file: import.meta.url,
  server: {
    // …entry, points, outdir…
    bunBuildConfig: { minify: false }, // override the always-minified server
  },
  client: {
    // …indexHtml, app, points, outdir…
    bunBuildConfig: ({ mode }) => ({
      drop: mode === 'production' ? ['console', 'debugger'] : [],
    }),
  },
})

To switch a side to Vite/Rolldown, give that side a viteConfig. The choice is per side: build the client with Vite while the server stays on Bun (or vice versa) by setting the option on the side you want, not at the top of the config:

export const engine = Engine.create({
  file: import.meta.url,
  server: {
    // …no viteConfig here → server stays on Bun…
  },
  client: {
    // …viteConfig here → only the client switches to Vite…
    viteConfig: ({ plugins, side }) => ({
      resolve: { tsconfigPaths: true },
      plugins: [
        ...plugins, // Point0's compiler plugin is already in here
        react({ include: /\.(jsx|js|mdx|md|tsx|ts)$/ }),
        tailwindcss(),
        side === 'client'
          ? analyzer({ analyzerMode: 'static', openAnalyzer: false })
          : null,
      ],
    }),
  },
})

Like bunBuildConfig, viteConfig is a plain Vite UserConfig (object or ({ plugins, side, mode }) => …). The function form receives plugins (Point0's compiler plugin is already included) and side ('server' or 'client'). examples/vite puts one viteConfig at the top level so both sides build on Vite. When to pick which is on Bun or Vite; the asset-pipeline differences are on Assets.

For the Bun path, vite and esbuild are kept external in the server bundle — a Bun-only app builds and runs without either installed.

The build is production by default

build defaults to NODE_ENV=production. That's what enables minification and external source maps. Build in any other mode and Point0 warns you:

point0 build --mode development
# WARN  Building with NODE_ENV=development, not "production": the client gets
#       inline sourcemaps and unminified bundles. Intentional? If not, set
#       NODE_ENV=production

Mode resolution (--mode > --env NODE_ENV=… > shell NODE_ENV > the command default) decides which .env files load and which env consts get inlined into the bundle. Full mode/env rules are on Engine config.

Two kinds of env behave differently at build time:

  • env.consts are inlined. At compile time the bundler replaces each process.env.X with the literal value ("prod"), so the const is frozen into the bundle and survives minification/tree-shaking. The client also gets these consts written into dist/client/index.html. Change a const → you must rebuild.
  • env.vars are NOT inlined. They stay real environment variables read at run time. The client receives them as window.__POINT0_ENV_VARS__ (injected into the served HTML by the running server, not baked into the JS); on the server they're plain process.env values from the machine you run on.

One internal const matters at runtime regardless: POINT0_BUILT='true' is defined into the server bundle and flips the engine into built mode — in built mode engine.preload skips the dev-only work (no plugin loading, no dev server, no NODE_ENV fallback) but still applies your server env.vars to process.env, and module preload links inject. See Engine runtime.

Running the build

The server entry name comes from your engine's server.entry — main becomes dist/server/index.server.js, and that's exactly the file you run:

export const engine = Engine.create({
  file: import.meta.url,
  server: {
    entry: { main: './index.server.ts' }, // → dist/server/index.server.js
    // …points, outdir…
  },
})
// examples/basic/package.json
"build": "point0 build",
"start": "bun run ./dist/server/index.server.js"

The built server serves dist/client at /, so the HTML shell, the JS chunks, and the hashed assets (GET /_point0/assets/<hash>) all resolve out of the client build. Run it with NODE_ENV=production in the environment — a built server started with no NODE_ENV falls into Bun's dev cascade. In production that var is part of the machine's environment, so the start script above doesn't set it inline. Deployment, including Docker, is on Deploy.

Source maps

Production client builds emit external source maps (.map files), and after the Bun client build Point0 re-chains them so a browser stack trace points at your original source, not the compiler's intermediate transform:

point0 build
# Source maps: re-chained 12/18 client chunk maps to original source

The reason: Bun's bundler composes its bundle→intermediate map but drops the compiler's inline intermediate→original map (Bun #6173), so the raw maps would resolve to generated code (react/compiler-runtime, _temp, _c(). The post-build pass (chainBundledSourceMaps) composes the two with @jridgewell/remapping and relativizes the sources. It only runs when Point0's compiler ran on the build, and it's a no-op when there's nothing to chain.

Source-map mode is per side and per mode:

BunVite
Client, productionexternal (.map files)hidden (.map, no sourceMappingURL comment)
Client, dev buildinlinetrue
Serverlinkedtrue

The client dir is served publicly, so a common production flow uploads the client .map files to an error tracker (e.g. Sentry) and then deletes them, while keeping the server maps (the server runs, it isn't served). Dev source maps are a separate mechanism — see Dev.

Module preload manifest

Each client build writes dist/client/_point0/<scope>/preload-manifest.json (<scope> is that client's own scope), a map from page name to the JS chunks that page needs. The built server reads it and injects <link rel="modulepreload"> tags so a page's chunks download in parallel with the HTML instead of after hydration:

{
  "entry": "/chunk-abc.js",
  "entryPreload": ["/chunk-def.js"], // entry's static closure (empty on Vite)
  "byPoint": { "idea": ["/chunk-idea.js"] }, // per-page lazy chunk + its closure
}

The manifest is computed from the bundler's chunk graph (Bun's metafile, or Rolldown's output). Mapping a page to its chunks needs the in-memory compiler points, so engine.build feeds each client its page sources right before bundling. The whole thing is best-effort: a glitch resolving sources logs a warning and falls back to entry-closure-only preload; it never fails the build.

Set POINT0_MODULE_PRELOAD=false (also 0 / off) to disable both the manifest and its injection. The manifest is only served in the built prod runtime — a stale one is ignored in dev.

Watch mode

point0 build --watch rebuilds on change. With no glob it watches the import graph of your build entries — it resolves what your server entries and each client's <script src> actually import, so a deep source edit triggers a rebuild without you listing files:

point0 build --watch
# rebuilds when any file in the entries' import graph changes

A glob value (--watch '<glob>', comma-separated or repeated) and the config's buildWatchGlob are additive on top of the import-graph watch, not a replacement. Each rebuild re-spawns a fresh point0 build child and re-collects the import graph, since an edit can add or remove imports. Use --keep-alive when a build plugin runs a long-lived server (like a bundle-analyzer on :8888) that you want to stay up after the build.

Partial builds

Build one side or one scope instead of everything:

point0 build --side client        # only the client bundle
point0 build --side server        # only the server bundle
point0 build --scope root         # only the 'root' scope

Gotcha with url-mode assets. The bytes for url-mode assets are written only by the client build (the server build references the same /_point0/assets/<hash> string but writes no bytes). So --side server alone does not produce servable assets — /_point0/assets/<hash> 404s unless a client build already populated dist/client. Build both sides (the default) whenever the app uses url-mode assets; only ?file assets are self-contained on a server-only build. See Assets.

Clean and publicdir

By default the build cleans each outdir before bundling (the clean runs up front, in parallel, so the actual builds skip it). Two flags turn the housekeeping off:

point0 build --no-clean       # keep the existing dist/ (incremental over it)
point0 build --no-publicdir   # skip copying the publicdir into the client outdir

The publicdir is copied into the client outdir before the JS build, so on a name collision the built dist files win over a static file. See Publicdir.

Reference

point0 build flags

From the CLI (packages/engine/src/cli.ts). Each maps to an engine.build() / engine.buildWatch() option.

FlagWhat it does
--engine <path>Path to the engine file (absolute or relative to cwd).
-w, --watch [glob]Rebuild on change. No value → watch the entries' import graph; a glob is additive on top of that and buildWatchGlob.
--side <side>Build only one side: server or client.
--scope <scope>Build only the matching scope.
-C, --no-cleanDo not clean the outdir before building.
-P, --no-publicdirDo not copy the publicdir into the client outdir.
-k, --keep-aliveDon't exit after the build — wait for Ctrl+C so long-lived build plugins (e.g. a bundle-analyzer server) keep running.
--env <name=value>Define env vars, repeatable; these override .env file values.
--mode <mode>NODE_ENV mode: production | development | test. Decides which .env files apply. Default for build is production.

Without --keep-alive, the process ends naturally once build plugins flush, with a 5-second safety-net force-exit.

engine.build()

await engine.build({
  side, // 'server' | 'client' — default: both
  scope, // restrict to one scope — default: all
  clean, // default: true
  publicdir, // default: true
})
// => {
//   clients: Array<{ client, publicdir, scope }>,
//   server: { server, publicdir },
// }

The arrays hold the written file paths per side (null when that side was skipped). engine.buildWatch() takes the same options plus the watch glob. Both refuse to run from an already-built engine (You can not build by built engine). The instance methods are on Engine runtime.

Per-side build config

Knobs you set on general / server / client in the engine config (full reference on Engine config):

OptionEffect
outdirOutput dir for that side (e.g. '../dist/server', '../dist/client').
viteConfigObject or ({ plugins, side }) => UserConfig. Presence switches the side to Vite.
bunBuildConfigPartial<Bun.BuildConfig> or ({ mode, side }) => …, spread over Point0's Bun defaults (override target, minify, …).
bunPluginsBun plugins for the build (e.g. 'bun-plugin-tailwind').
bannerA string merged into the bundle banner alongside the injected env script.
buildWatchGlobExtra glob(s) watched in --watch, additive on top of the import graph.
compiler / compiler.assetsThe asset pipeline that rides inside the build; compiler: false falls back to native bundler asset behavior. See Assets and Compiler.
НазадDevДалееCompiler

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