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 flagsTailwind to StyleX
Nifra includes a conservative codemod for moving static Tailwind classes to StyleX. It is a dry run by default, so inspect the proposed edits first:
nifra migrate --from tailwind --to stylex # report safe edits and manual issues
nifra migrate --from tailwind --to stylex --write # apply only safe edits
nifra migrate --from tailwind --to stylex --dir srcSupported static className attributes become stylex.props(...) calls backed by a local stylex.create(...) table. Responsive breakpoints and element-local pseudo-classes such as hover: are supported. Dynamic class expressions, arbitrary values, parent-dependent variants, and unknown utilities stay unchanged and are reported with file/line evidence. A non-zero exit status means manual migration issues remain.
StyleX compilation is native to Nifra's Bun pipeline. Install the runtime and compiler peers, then register both the browser and SSR transforms:
# Install the runtime and the optional compiler peers.
bun add @stylexjs/stylex
bun add -d @babel/core @stylexjs/babel-plugin @babel/plugin-syntax-flow \
@babel/plugin-syntax-jsx @babel/plugin-syntax-typescript// nifra.config.ts - Bun's client and SSR build transforms.
import { stylexBunPlugin } from "@nifrajs/web/plugins/stylex"
export const clientPlugins = [stylexBunPlugin("dom")]
export const serverPlugins = [stylexBunPlugin("ssr")]If the project intentionally uses Vite, add the Vite adapter alongside its existing Vite transforms. Nifra will use Vite when vitePlugins are the app's only transforms:
// doc-check: skip - requires the third-party Vite React plugin
// If the app intentionally uses Vite, keep StyleX in vitePlugins.
import react from "@vitejs/plugin-react"
import { stylexVite } from "@nifrajs/web/plugins/stylex"
export const vitePlugins = [react(), stylexVite()]Scope
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.