CLI
Nifra is zero-config: it reads routes/, framework.ts, and (optionally) backend.ts from your project and wires the right @nifrajs/web entrypoint - no dev.ts/build.ts/server.ts to hand-write. (`create-nifra` scaffolds the conventions.)
nifra dev # true-HMR dev server (Bun native HMR + Nifra SSR) - http://localhost:4321
nifra dev --vite # the Vite middleware pipeline instead (automatic when vitePlugins are your ONLY transforms)
nifra build # full Bun deploy → dist/server.js + content-hashed dist/assets/ (default target: bun)
nifra start # run dist/server.js on Bun
nifra build --target cf-pages # also: node | deno | vercel | static; add --report for chunk sizes
# dev + start share the default port 4321. Override per run: --port <n> (alias -p) or the PORT env var.
# flags: --port <n> (dev/start) · --out <dir> (build/start) · --target <t> (build) · --poll (dev)
# --vite | --bun (dev/build) force the pipelineThe conventions
Four conventions at the project root. nifra dev runs the Bun-native HMR dev server; nifra build runs the Bun-native production build (content-hashed client assets plus a target-specific server); nifra startruns the default Bun output. The generated server serves assets and SSR with matched-route chunks preloaded and route CSS linked in each <head>.
One rule picks the bundler, and it is the same rule in both phases - so a project can never be bundled by one toolchain in dev and the other in production. Bun runs everything, except an app whose only transforms are vitePlugins: the Bun pipeline cannot run those, so that app gets Vite for dev and build alike, and says so. --vite / --bun force the choice; --bun is refused on a Vite-only app rather than dropping its transforms silently. Every dev and build run prints which bundler it picked and why, and nifra check answers the same question without starting a server - see which pipeline runs, when for the full table.
my-app/
routes/ # file-based routes (index.tsx, _layout.tsx, [id].tsx, …)
framework.ts # deploy-safe render adapter
nifra.config.ts # CLI-only client module + dev/build plugins
backend.ts # export const backend = server()... (optional - the typed contract)These paths are fixed, not configurable: the CLI resolves all four from the project root, so backend.ts cannot move into src/ or be renamed - if the file is not at the root, nifra dev|build simply run without a backend (it is optional, so nothing errors). Only the entry file is pinned; the feature modules it .merge()s can live in any directory. Keep backend.ts a pure composition root - merging modules, registering no routes of its own - so each route's capability reach stays its own module's (see Backends & API and Verification).
framework.ts - naming the framework once
Keep the render adapter in deploy-safe framework.ts. Put clientModule, Vite plugins, compiler plugins, conditions, and defines in CLI-only nifra.config.ts. This prevents build tooling and native dependencies from entering generated server bundles.
// framework.ts - deploy-safe; generated server entries import this file.
import { reactAdapter } from "@nifrajs/web-react"
export const adapter = reactAdapter
// nifra.config.ts - CLI-only build/dev tooling; never imported by a deployed server.
// doc-check: skip - needs the third-party @vitejs/plugin-react; install it to run this.
import react from "@vitejs/plugin-react"
export { adapter } from "./framework"
export const clientModule = "@nifrajs/web-react/client"
export const vitePlugins = [react()] // dev HMR (Fast Refresh)
// Vue/Svelte/Solid also export:
// clientPlugins = [vueBunPlugin("dom")] // compile routes for the client build
// serverPlugins = [vueBunPlugin("ssr")] // compile routes into the target's server bundle
// conditions = ["solid"] // Solid: resolve solid-js to its source
// define = { __VUE_OPTIONS_API__: "true", ... } // Vue feature flagsScope
nifra build defaults to a self-hosted Bun server. Use --target node, deno, cf-pages, vercel, or static for another complete deploy shape (see Deployment). Run non-Bun outputs with the command printed by the build.