next_build¶
Wraps next build as a single Bazel action. The action stages a Next.js project
directory from declared inputs alone — the sources, a node_modules tree, the
config, an optional tsconfig.json — runs next build in it with the network
blocked, and returns the .next/ directory as one output artifact.
Usage¶
load("@rules_typescript//npm:defs.bzl", "node_modules")
load("@rules_typescript//ts:defs.bzl", "next_build")
node_modules(
name = "node_modules",
deps = [
"@npm//:next",
"@npm//:react",
"@npm//:react-dom",
"@npm//:types_node",
"@npm//:types_react",
"@npm//:typescript",
],
)
next_build(
name = "app",
srcs = glob([
"src/app/**/*.tsx",
"src/app/**/*.ts",
"src/app/**/*.css",
"src/pages/**/*.tsx",
"src/pages/**/*.ts",
"src/middleware.ts",
"public/**",
]),
config = "next.config.mjs",
node_modules = ":node_modules",
tsconfig = "tsconfig.json",
)
Attributes¶
| Attribute | Type | Default | Description |
|---|---|---|---|
srcs |
label_list |
required | Every project file Next.js reads: routes, components, stylesheets, middleware.ts, public/ assets |
node_modules |
label |
required | A node_modules target holding next, react, react-dom, typescript, the @types packages and the app's dependencies |
staging_srcs |
label_list |
[] |
Files from other targets, staged at their workspace-relative paths |
config |
label |
None |
The next.config.{mjs,js,ts} file |
config_srcs |
label_list |
[] |
Modules config imports, staged beside it |
tsconfig |
label |
None |
A tsconfig.json staged into the project root |
env |
string_dict |
{} |
Extra environment variables for the build |
allow_network |
bool |
False |
Let next build reach the network. See Hermeticity |
Each srcs file lands at its path relative to the target's package; a file from
another package has no path inside the project root and fails with an error
naming staging_srcs. next build npm-installs typescript and the @types
packages itself when they are missing, and the action has no network to do it
with, so the node_modules tree carries them. Without config, Next.js uses
its defaults; without tsconfig, it writes its own default and any paths
aliases the app relies on are gone. config is a single file, so a config with
a sibling import fails with ERR_MODULE_NOT_FOUND until that sibling is listed
in config_srcs. NEXT_TELEMETRY_DISABLED and NEXT_PRIVATE_SKIP_PATCHING are
always set.
Files srcs must list¶
The staging directory contains the declared inputs and nothing else, so an omission surfaces as a module-resolution failure. Three are easy to miss:
- Stylesheets.
import "./globals.css"isModule not found: Can't resolve './globals.css'until**/*.cssis insrcs. public/. A staticimport logo from "../../public/logo.png"isModule not founduntilpublic/**is insrcs. Nothing else needspublic/at build time — see Serving the output.middleware.ts. It sits besideapp/, not under it, so anapp/**glob misses it. Nothing imports it, so no failing import names the omission. Assert on the output: the compiled middleware underserver/and the matcher inserver/middleware-manifest.json.
App Router pages and layouts, route.ts handlers, Pages Router pages with
getServerSideProps, pages/api/*, and a "use client" component calling a
"use server" action all compile with no extra attrs.
Gazelle-generated srcs¶
The Gazelle extension writes srcs as a glob() over the directories Next.js
owns and recomputes it on every run, so it covers the three above. It also
owns the attribute: a pattern you add that Gazelle does not derive is
dropped on the next run unless a # keep holds it, and the same goes for
staging_srcs, config and tsconfig.
The run names every value it drops. Full contract: Attributes Gazelle owns on the framework rules.
staging_srcs¶
staging_srcs takes any target's files, which gives two shapes for a shared
package:
staging_srcs = [
"//packages/shared/src:sources", # a filegroup of .ts — Next.js compiles them
"//packages/shared/src", # a ts_compile target — Next.js bundles its .js
]
The second is the hybrid boundary: ts_compile stages index.js beside the
index.d.ts that types it, so Next.js bundles already-compiled JavaScript and
type-checks the import through the declaration. Shared code compiles once and is
cached by Bazel.
Either way the files land at their workspace-relative paths, so a relative
import (../../packages/shared/src/index) resolves with no transpilePackages
rewriting.
Under Gazelle, staging_srcs is generated from the packages it finds outside the
owned directories. A label it cannot derive, like a filegroup you wrote over a
vendored tree, needs a # keep on its line:
Hermeticity¶
The action runs with block-network, because next build reaches for the
network on its own initiative. Enforcement is the sandbox's: only a sandbox that
can create a network namespace of its own provides it. --spawn_strategy=local
has no sandbox and so no boundary, and processwrapper-sandbox — Bazel's
fallback where unprivileged user namespaces are unavailable, including a Bazel
nested inside another Bazel's sandbox — honours the requirement by ignoring it.
The requirement is on the action either way, which
bazel aquery 'mnemonic("NextBuild", //:your_target)' shows.
next/font/google is the one common feature this rejects: it downloads the font
CSS and the woff2 payloads while compiling. next/font/local is unaffected: the
font file is an input like any other, and lands in static/media/.
The wrapper names the cause; the failure would otherwise arrive as an
ENETUNREACH from inside a webpack loader:
next_build: `next build` failed while reaching for the network.
...
Either declare the font locally -- `next/font/local`, with the font files listed
in `srcs` -- or set `allow_network = True` on this next_build target to accept a
build whose output depends on the network.
The diagnostic is keyed on the build log: the wrapper prints it when a failed
build's output matches ENETUNREACH, EAI_AGAIN, ECONNREFUSED, getaddrinfo
or from Google Fonts. A failure matching none of them exits with Next.js's own
message alone.
allow_network = True swaps block-network for requires-network. The
target's output then depends on a remote host.
Reproducibility¶
Two builds of identical inputs do not produce identical bytes:
- Next.js bakes the absolute project path into its server bundles. Under sandboxing that path includes the sandbox run number.
BUILD_IDis a random nanoid unlessnext.configsetsgenerateBuildId.- The server-actions encryption key is a fresh AES-GCM key per build unless
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYis set, which theenvattr can do. Pin it: two builds otherwise disagree about how to decrypt an action's arguments, which a rolling deploy notices.
Output¶
One directory artifact, <name>_next_out, holding the .next/ tree: server/
(route bundles, prerendered HTML, the route and middleware manifests), static/
(client chunks, css/, media/), BUILD_ID, and the top-level manifests
next start reads.
The consumer is next start, which reads the tree by name, and the file set
depends on which routes Next.js prerendered, so the pruning is subtractive.
These are removed:
| Removed | Why |
|---|---|
cache/ |
A local incremental cache. Machine-specific, and large |
trace |
Every build span, tagged with the absolute staging path and a timestamp |
diagnostics/ |
Build timings |
Nothing serves from any of them. Everything else next build writes stays.
Where each convention lands:
| Convention | Output |
|---|---|
| App Router page | server/app/<route>/page.js, plus server/app/<route>.html and its .rsc/.meta siblings when prerendered |
| Route handler | server/app/<route>/route.js |
| Pages Router page | server/pages/<route>.js, or just <route>.html when static |
| Pages API route | server/pages/api/<route>.js |
"use client" |
server/app/<route>/page_client-reference-manifest.js |
"use server" |
An entry in server/server-reference-manifest.json under layer action-browser |
src/middleware.ts |
server/src/middleware.js + server/edge-runtime-webpack.js, with the matcher in server/middleware-manifest.json |
| Imported CSS | static/css/<hash>.css |
| Statically imported image | static/media/<name>.<hash>.<ext>, referenced as /_next/image?url=… |
Which routes were prerendered and which stayed dynamic is in
prerender-manifest.json: a force-dynamic route is absent from it.
Serving the Output¶
next build does not copy public/ into .next; Next.js serves it from the
project directory at request time. Serving also needs the config, a
package.json and the npm tree beside the output. What runs the artifact is
outside this rule.
Type Checking¶
next build runs TypeScript itself, over the staged tsconfig.json. A type
error in any staged file fails the Bazel action, including a file that arrived
through staging_srcs. There is no separate validation action and no deps
attr.