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, eachscopeorscope=value;1meansboot. 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.jsonlpath collects the heavy-package facade profile instead — the two are never wanted at once, so the suffix picks.vendor=<path>appends the JSONL workload tracebench/tools/vendor-analyze.tsreads.