@cfxlabsinc/nx-open-nextWorkspace-internal Nx plugin for the Next.js apps packaged with OpenNext.
createNodes watches **/open-next.config.ts and contributes four targets. Every Next command runs as bun --bun next …, on Bun's runtime: a bare bun next honours the bin's #!/usr/bin/env node and runs on Node.
| Target | Command | Notes |
|---|---|---|
build |
bun --bun next build |
Not cached, standalone. dependsOn: ["typecheck", "^build"] |
open-next-build |
bun --bun open-next build |
Cached. Runs the same next build, then packages — see below |
serve |
bun --bun next dev / next build + next start |
development (default) and production configurations |
analyze-bundle |
bun --bun next experimental-analyze |
Serves the analyzer. -c ci writes a snapshot and exits — see below |
build sets tty: false because Next renders a progress UI on a TTY that corrupts Nx's captured output.
typecheck is not this plugin's: @cfxlabsinc/nx-bun-check infers it for every package, the dashboards included. A dependsOn naming an absent target is skipped silently, so build and open-next-build rely on that plugin being registered.
.next for every buildEach app keeps one dist dir, <app>/.next, Next's default. SST (nextjs.ts) and OpenNext read the build only from there, so it is the one place every consumer can share. build, serve -c production and OpenNext's buildCommand run the same next build (standalone, NEXT_PRIVATE_STANDALONE=true) into it, so each starts from the Turbopack cache (.next/cache) the last one left. next dev writes .next/dev, which next build keeps when it cleans the dir (with cache and diagnostics); Next keeps the dev and build Turbopack caches apart on purpose.
build is not cached in Nx, and must not be. Its output holds symlinks: Turbopack's hashed externals in .next/node_modules (@sentry/server-runtime-injection-<hash>, @valkey/valkey-glide-<hash>) and Bun's store under .next/standalone/node_modules. Nx's cache drops symlinks on restore (measured 2026-11-12: 55 links before, 0 after). A restored build 500s every route under next start (Cannot find module '@sentry/server-runtime-injection-…/register'), and OpenNext packages it into a server function with no node_modules/next (839 files against 2,078). Speed comes from the warm .next/cache, which is local and never restored. CI persists it with next-build-cache.open-next-build is cached, and guarded. .open-next is plain files. Its outputs are exactly what SST reads: .open-next plus .next/BUILD_ID, routes-manifest.json and prerender-manifest.json. The command fails when .open-next/server-functions/default/node_modules/next is missing, since OpenNext swallows every file-copy error into a debug log and exits 0.OPEN_NEXT_BUILD_ENV is part of its hash. Each stage inlines its own NEXT_PUBLIC_* values, so without them a preview's entry could be restored into a dev or prod deploy of the same commit. tools/ci/open-next-build-env.test.ts fails when the workspace reads a NEXT_PUBLIC_* the list does not name.CFX_SKIP_NEXT_TYPECHECK=1 drops Next's own type check, which re-checked the same files as typecheck (~75s of an admin build). dependsOn: ["typecheck"] keeps the gate.turbopackPluginRuntimeStrategy: "workerThreads" is required under Bun, in every app's next.config.mjs. The default runs Turbopack's JS loaders (Sentry's among them) in node children with process.execArgv in NODE_OPTIONS; under bun --bun that is --bun, Node exits 9 (--bun is not allowed in NODE_OPTIONS), and the first compile hangs at 0% CPU.analyze-bundle, and its ci configurationThe default serves Next's Turbopack bundle analyzer on a port. nx run <app>:analyze-bundle:ci runs next analyze --output --snapshot ci instead, which writes the snapshot to dist/packages/<app>/.next/diagnostics/analyze and exits. The PR bundle size comment is built from it (tools/scripts/bundle-report.ts).
dist/packages/<app>/.next (NEXT_DIST_DIR). In build's .next the two evict each other: next analyze builds with its own build id and no rewrites or encryption key, and the cache holds one variant at a time. Measured on ui-sandbox (2026-11-12, Bun): analyze after a warm build compiled cold (7.0s), the build after it cold again (7.8s against 2.1s warm), and the cache stayed ~165-185 MB instead of growing to hold both.dependsOn. The analyzer compiles from source and reads no env: every @cfxlabsinc/* export resolves to a .ts file, so nothing upstream has to be built first. admin-dashboard:e2e and ui-sandbox:visual run next build on a clean checkout the same way.ui-sandbox and 460 MB for admin-dashboard, and it is an intermediate: CI keeps only the summary.--dist-dir: bun next analyze export --snapshot ci --dist-dir ../../dist/packages/<app>/.next. The export defaults to <app>/.next, where nothing is written.next build's (3 of ui-sandbox's 69 client files shared a name), so a snapshot's file cannot be looked up in .next/static.open-next.config.ts is the markerNot next.config.*: the marker has to mean "a Next app that OpenNext packages for a Lambda/CloudFront deploy", which is what makes open-next-build meaningful at all. A plain Next app would want build and serve but not that. Today the file sits in admin-dashboard, bank-dashboard, customer-dashboard and ui-sandbox. @cfxlabsinc/nx-sst keys deploy and cancel on sst.config.ts.
serve takes the port as an nx:run-commands option rather than baking it into the command, so a project overrides just the number:
"serve": { "options": { "port": 3100 } }
Nx merges a project.json target's options into the inferred ones key by key, so cwd and both configurations survive — the override is only the port. It applies to development and production alike, since both interpolate {args.port}.
The default is 3000 (Next's own default), and can be changed workspace-wide via the plugin's defaultPort option in nx.json:
{ "plugin": "@cfxlabsinc/nx-open-next", "options": { "defaultPort": 4000 } }
This replaced customer-dashboard's hand-written serve, which existed solely to pass --port 3000. Each dashboard now declares its own port, so all three can run at once:
| Project | Port |
|---|---|
customer-dashboard |
3000 |
bank-dashboard |
3200 |
admin-dashboard |
4200 |
Registered in workspace nx.json under plugins:
{
"plugin": "@cfxlabsinc/nx-open-next",
"options": {}
}
Resolves through the workspace dependency in the root package.json:
"@cfxlabsinc/nx-open-next": "workspace:*"