Development

The pnext CLI carries the whole flow: dev while you build, analyze to inspect what ships, build and start for production.

pnext dev [directory] [--port 3000] [--hostname 127.0.0.1]
pnext build [directory] [--adapter vercel] [--verbose]
pnext start [directory] [--port 3000] [--hostname 127.0.0.1]
pnext analyze [route] [directory] [--why <package>] [--brotli] [--files] [--json]
pnext typegen [directory]
pnext create <directory> [--no-install]
pnext migrate [directory] [--dry-run]

create scaffolds a new app with everything set up. migrate converts a Next.js project in place: it rewrites package.json and tsconfig.json and creates a pnext.config.ts with compat.next enabled, without editing app source; --dry-run previews. To set a project up by hand, add the package and scripts:

{
  "devDependencies": {
    "@wular/pnext": "latest"
  },
  "scripts": {
    "dev": "pnext dev",
    "build": "pnext build",
    "start": "pnext start",
    "analyze": "pnext analyze"
  }
}

Dev server

pnext dev starts a Bun HTTP server that scans the route tree, renders matching pages on request, and serves static assets from public/. Client route entries are built on demand and cached in .pnext/cache. Browser pages reload on route-tree changes over a server-sent events stream at /__pnext/events; the other /__pnext/* endpoints are implementation details, not application routes.

The dev server does not typecheck in the request path; run your package's lint and typecheck scripts separately. pnext typegen regenerates the route types on demand; see Type Safety.

In Activity Monitor and ps, the dev server runs as pnext-dev, builds as pnext-build, and the bundler service as pnext-esbuild. The dev server re-execs itself when its memory passes PNEXT_DEV_MAX_RSS_MB (default 2048), and restarts the bundler past PNEXT_DEV_MAX_ESBUILD_RSS_MB (default 1024).

Analyze

pnext build ends with each page's initial load: the gzipped HTML, CSS, and JavaScript a first visit downloads, with the change since the previous build and the command to inspect the heaviest route.

pnext analyze lists the files behind every route. Pass a route, either a template like /users/[id] or a concrete path, for the full report on it:

  • Chunks by content: each chunk's packages by share, marked when the build named the file after a package that is a minority of it.
  • Packages: the initial JavaScript by package, with its gzip share and the import chain that pulls it in.
  • CSS: global and route stylesheets, how many utilities each holds and how many the route references, and which Tailwind sources produce them: the automatic scan or each @source.
  • Fonts: faces with their files and sizes, whether each is preloaded, and whether anything on the route uses it.
  • Flags: framework runtime the route does not need, chunks under 1 KB, CommonJS and whole-barrel imports, packages bundled into several chunks or installed twice, preloaded fonts nothing uses, and budget overruns.

--why <package> prints the chain that pulls a package into each route:

$ pnext analyze /onboarding --why zod
zod <- @hookform/resolvers/zod <- app/onboarding/create-org.tsx <- /onboarding

--brotli measures with brotli instead of gzip, --files lists the files no route loads, and --json emits the whole report. Analyze reads only the build output, so it runs without the app's environment variables; an app with a custom outDir still loads its config to find the output.

To hold a route to a size, set a gzip budget per route with budgets. The build warns about a route over its budget, and analyze flags it.

Build and run

pnext build makes the production build and pnext start serves it. Routes render on the server per request; ones that never read the request are prerendered to static HTML at build time. A compat.next build typechecks off-thread alongside bundling.

Debug flags: --experimental-build-mode compile|generate splits the build into its two phases, --debug-build-paths <paths> narrows diagnostics to matching paths, and --debug-prerender prints prerender diagnostics.

Deploy

pnext deploys anywhere Bun runs: a VPS, a container, or any host you control. Run pnext build, then ship <outDir>/standalone; it needs nothing else. Serve it with pnext start, or import standalone/server/entry.js in your own server and call createRequestHandler(). To use a CDN, upload standalone/static/ as-is.

Vercel has a dedicated adapter: pnext build --adapter vercel writes Build Output to .vercel/output. Static pages and static route-handler responses are emitted as files; everything dynamic runs in a single _pnext function on Vercel's Bun runtime.

Environment variables

  • PNEXT_COMPAT=next: Next compatibility without a config file. See Compatibility.
  • PNEXT_TYPECHECK=classic: in-process TypeScript checker instead of the native one.
  • PNEXT_TRACE=<scopes>: turn on diagnostic output. Comma-separated scopes, each scope or scope=value; 1 means boot. A scope is all-or-nothing — it turns on everything it covers. Example: PNEXT_TRACE=boot,server=/tmp/modules.json.
scope prints / does
boot boot-phase timings and memory readings
server dev request, render and build timings; per-module compile attribution; route-facts and global-css hit/miss; why each artifact was recompiled; load-plugin registrations
vendor vendor saturation spans, esbuild plugin-callback attribution, and the preplan / native decision report
client client-build phase timings, plus the esbuild metafile written into the output directory
all the four above

Two scopes take a =<path> value:

  • server=<path> dumps the per-module compile list as JSON. A .jsonl path collects the heavy-package facade profile instead — the two are never wanted at once, so the suffix picks.
  • vendor=<path> appends the JSONL workload trace bench/tools/vendor-analyze.ts reads.