tomato·bazeldocs v0 · latest
Docs/Reference/Modules/rules_nextjs

rules_nextjs

Bazel rules for Next.js. Hermetic 'next build' with .next/ as a declared output directory.

Latest0.3.0
Versions4
CategoryBazel rules
Compat level1
MaintainersMatt Marshall
Registryhttps://registry.tbzl.dev/modules/rules_nextjs/
Sourcegithub.com/tomato-bazel/rules_nextjs
MODULE.bazelstarlark
bazel_dep(name = "rules_nextjs", version = "0.3.0")

View source & releases on GitHub ↗

Bazel rules for Next.js. Runs next build as a hermetic Bazel action with the workspace’s deps as explicit inputs and the .next/ tree as the declared output.

  • rule: next_build — see docs/defs.md.
  • rule/macro: next_standalone — turn an output = "standalone" build into a bazel run-able server and a deployable bundle for pkg_tar/oci_image.
  • provider: NextBuildInfo — wraps the .next output tree so future rules (deploy targets, oci_image wrappers, doc-site extractors) can consume builds programmatically.

Install

Add the registry to your .bazelrc:

common --registry=https://raw.githubusercontent.com/fastverk/bazel-registry/main/
common --registry=https://bcr.bazel.build/

In your MODULE.bazel:

bazel_dep(name = "rules_nextjs", version = "0.1.0")

You’ll also need aspect_rules_js (or equivalent) to expose next as a js_binary-compatible target — this rule consumes the CLI via next_bin, doesn’t bring its own.

Quick start

load("@npm//:my-app/next/package_json.bzl", next_bin_gen = "bin")
load("@rules_nextjs//next:defs.bzl", "next_build")

# Real js_binary wrapping node_modules/next/dist/bin/next. The rule needs
# an executable target — `:node_modules/next/dir` is a directory and
# cannot be exec'd directly. aspect_rules_js generates `bin.next_binary`
# for any npm package that declares a bin in its package.json.
next_bin_gen.next_binary(name = "next_cli")

next_build(
    name = "build",
    srcs = glob(["src/**/*", "public/**/*"]) + [
        "next.config.ts",
        "tsconfig.json",
    ],
    deps = [
        "//packages/some-lib:lib",
        ":node_modules/next",
        ":node_modules/react",
        ":node_modules/react-dom",
    ],
    data = [
        # Runtime assets dropped into public/ before the build.
        "//db/migrations:bundle",
    ],
    next_bin = ":next_cli",
)

bazel build //:build produces bazel-bin/build.out/ containing the full .next/ tree (standalone/, static/, trace files).

Standalone: runnable server + deployable bundle

next build’s output: 'standalone' emits the self-contained server (.next/standalone) and the hashed client assets (.next/static) as siblings — neither runs on its own. next_standalone re-stitches them into one tree (matching the hand-written Dockerfile COPY layout) and exposes it two ways:

load("@rules_nextjs//next:defs.bzl", "next_build", "next_standalone")

next_build(
    name = "build",
    # ... as above ...
    output = "standalone",  # the default; static/vercel get no runnable
    next_bin = ":next_cli",
)

next_standalone(
    name = "app",
    build = ":build",
    next_bin = ":next_cli",  # borrowed for the hermetic Node
)
  • bazel run //:app — serve the app on the hermetic Node (honors PORT / HOSTNAME).

  • //:app.bundle — a TreeArtifact ready for an image:

    load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
    load("@rules_oci//oci:defs.bzl", "oci_image")
    
    pkg_tar(name = "app_layer", srcs = ["//:app.bundle"], package_dir = "/app")
    oci_image(
        name = "image",
        base = "@distroless_nodejs",
        tars = [":app_layer"],
        workdir = "/app",
        # The bundle drops a fixed-name entry shim at its root (it discovers the
        # nested server.js for you), so the cmd never changes per app:
        cmd = ["__next_standalone_server.cjs"],
    )

The standalone server resolves /_next/static/* relative to the cwd, so the runnable and the entry shim both cd to the bundle root, where .next/static is re-stitched.

next_build repairs the standalone node_modules so dynamic runtime requires (e.g. next’s require-hook → styled-jsx) resolve — without it the deref’d standalone crashes on boot. The trade-off is a heavier node_modules than a pnpm-built standalone; see the CHANGELOG 0.3.0 note.

Hermeticity

The rule forces three Next.js env vars:

  • NEXT_TELEMETRY_DISABLED=1
  • NEXT_PRIVATE_STANDALONE=1
  • NODE_ENV=production

The rest of the hermeticity scrub lives in each app’s next.config.tsrules_nextjs deliberately doesn’t try to patch from the outside. Consumer-side checklist:

Bring under controlHow
Font CDN fetchesVendor under public/fonts/ or use next/font/local; next/font/google reaches fonts.googleapis.com at build time
Image optimizer pre-fetchesimages: { unoptimized: true } or explicit remotePatterns
Build-time network from instrumentationAudit instrumentation*.ts for module-init side effects
Next versionPin via root package.json catalog

Validate the scrub by building with --network none after the migration lands.

Compatibility

  • Bazel: 7.4+, bzlmod required.
  • Next.js: 14+ tested. Earlier versions may work — next build <app-dir> and the env-var contract have been stable.
  • Workspace shape: assumes aspect_rules_js-style npm linking (:node_modules/next/dir).

Contributing

Reference docs (docs/defs.md) are stardoc-generated. After editing rule docstrings:

bazel run //docs:update

CI gates this via bazel test //docs/....

License

MIT.

Usage#

Real usage, taken from the module’s examples/.

examples/minimal-app/BUILD.bazel

load("@npm//:defs.bzl", "npm_link_all_packages")
load("@npm//:next/package_json.bzl", next_bin = "bin")
load("@rules_nextjs//next:defs.bzl", "next_build", "next_standalone")

package(default_visibility = ["//visibility:public"])

npm_link_all_packages(name = "node_modules")

next_bin.next_binary(name = "next_cli")

next_build(
    name = "build",
    srcs = glob([
        "src/**/*",
    ]) + [
        "next.config.mjs",
        "package.json",
        "tsconfig.json",
    ],
    deps = [
        ":node_modules/@types/node",
        ":node_modules/@types/react",
        ":node_modules/next",
        ":node_modules/react",
        ":node_modules/react-dom",
        ":node_modules/typescript",
    ],
    next_bin = ":next_cli",
    output = "standalone",
)

# `bazel build //:standalone.bundle` → deployable run tree (pkg_tar/oci input).
# `bazel run   //:standalone`        → serve it locally on $PORT (default 3000).
next_standalone(
    name = "standalone",
    build = ":build",
    next_bin = ":next_cli",
)

Rules & providers#

Generated with Stardoc from the module's .bzl sources.

from docs/defs.md

User-facing rules for rules_nextjs.

Currently exports next_build, which runs next build as a Bazel action with the workspace’s deps as inputs and .next/ as the declared output. Forces hermeticity-relevant Next.js env vars (NEXT_TELEMETRY_DISABLED=1, NEXT_PRIVATE_STANDALONE=1, NODE_ENV=production) so the build itself doesn’t drift.

The font/image-optimizer/instrumentation hermeticity scrub lives in each consuming app’s next.config.ts — the rule doesn’t try to patch it from the outside. See README.md for the consumer-side checklist.

Targets returning NextBuildInfo expose the .next tree programmatically so future rules (deploy targets, doc-site extractors, oci_image wrappers) can consume builds without re-running next build.

next_build

load("@rules_nextjs//next:defs.bzl", "next_build")

next_build(name, deps, srcs, data, app_dir, next_bin)

Run next build hermetically and emit the .next tree as a Bazel-output directory.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsts_project / npm link targets the app imports. Brought into runfiles for the build action.List of labelsoptional[]
srcsApplication source files + public/ assets + next.config.ts + instrumentation.*.List of labelsrequired
dataAdditional inputs that should land in the working tree before next build runs (e.g. migrations.zip -> public/).List of labelsoptional[]
app_dirPackage-relative app root. Defaults to the package containing the rule.Stringoptional""
next_binjs_binary-compatible target for the Next CLI (typically :node_modules/next/dir).Labelrequired

NextBuildInfo

load("@rules_nextjs//next:defs.bzl", "NextBuildInfo")

NextBuildInfo(tree)

A next build output tree.

FIELDS

NameDescription
treeDirectory: the .next output (standalone + static).

Conformance#

No gate findings. 15 contested atoms. See how gating works or the full report.

Contested atoms

Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.

AtomResolved hereElsewhere
apple_support 1.24.2 2.2.0 ×1
aspect_bazel_lib 2.22.5 2.8.1 ×1
bazel_lib 3.0.0 3.2.2 ×17
bazel_skylib 1.8.2 1.9.0 ×2
gawk 5.3.2.bcr.1 5.3.2.bcr.3 ×17
jq.bzl 0.1.0 0.4.0 ×17
nlohmann_json 3.6.1 3.12.0.bcr.1 ×1
package_metadata 0.0.2 0.0.5 ×3
protobuf 33.4 34.0.bcr.1 ×2
rules_jvm_external 6.7 6.8 ×4
rules_python 1.7.0 2.0.1 ×1
rules_swift 3.1.2 3.6.1 ×1
tar.bzl 0.5.1 0.10.4 ×170.6.0 ×1
upb 0.0.0-20220923-a547704 0.0.0-20230516-61a97ef ×1
yq.bzl 0.1.1 0.3.4 ×17

Dependencies#

rules_nextjs in the registry graph — what it depends on (left) and what depends on it (right).

Depends on

platforms0.0.10bazel_skylib1.7.1aspect_bazel_lib2.22.5rules_shell0.4.1devstardoc0.7.2dev

Versions#

4 published versions, newest first. Each resolves to an immutable, integrity-checked archive.

VersionIntegrity (sha256)Source archive
0.3.0 latest rd7BcyuPLWrq3o3d… tag archive ↗
0.2.0 j7f0a/KAICSfSSBb… tag archive ↗
0.1.1 uTiQDrfTq/axk35m… tag archive ↗
0.1.0 PQU+kTMPVCBk6/lD… tag archive ↗

Changelog#

All notable changes to rules_nextjs. The format is loosely Keep a Changelog — version headers mirror the published bazel-registry entries.

0.3.0 — next_standalone (runnable + deployable bundle)

  • next_build: add an output attribute (standalone | static | vercel | default, default standalone). Only standalone forces NEXT_PRIVATE_STANDALONE=1; the mode is surfaced on NextBuildInfo (along with app_dir) so downstream rules can gate on it.
  • New next_standalone macro: from an output = "standalone" build it emits <name>.bundle — the standalone server re-stitched with .next/static into one deployable TreeArtifact (feed to pkg_tar/oci_image) — and <name>, a bazel run-able launcher that serves the bundle on the hermetic Node. Static / vercel builds fail fast (no self-contained server). New NextStandaloneInfo provider + a fixed-name entry shim (NEXT_STANDALONE_ENTRY) so an oci_image cmd can target the server regardless of where Next nested server.js.
  • next_build: repair the standalone node_modules for runtime resolution. next build’s cp -RL promote step deref’s the standalone into real directories, which disconnects each package from its sibling deps under .aspect_rules_js/<key>/node_modules/ — so e.g. next’s require-hook can’t find styled-jsx and the server crashes on boot. The build now adds a flat top-level node_modules/<pkg> entry (relative, internal symlink) for every content-store package, so Node’s realpath walk always reaches the single top-level node_modules and resolves every transitive dep. next_standalone’s bundle preserves these symlinks (cp -R) so the image isn’t doubled. Verified: bazel run //:standalone on the example app serves HTTP 200.
  • Trade-off: against the aspect_rules_js content store, next build’s trace + the deref produce a large standalone node_modules. Correct, but heavier than a pnpm-built standalone — narrowing the trace is a separate optimization.

0.2.0 — next_dev + bundler selection

  • Add a next_dev rule: a bazel run-launched Next.js dev server in the workspace tree (companion to the hermetic next_build).
  • next_build: add a bundler attribute (webpack | turbopack) to select the Next.js bundler.
  • next_build: two-action design with tsconfig / next.config rewrites; the next.config wrapper handles peer-dep visibility, workspace transpilePackages, and the .ts extensionAlias.
  • New dependency: aspect_bazel_lib (2.22.5).

0.1.1 — hermeticity fixes for next_build

  • Fix BAZEL_BINDIR propagation, pass workspace deps as explicit action inputs, and switch to FilesToRunProvider so next build runs cleanly inside the Bazel sandbox.

0.1.0 — initial release

  • First cut of Bazel rules for Next.js: a next_build rule that runs next build as a hermetic Bazel action with workspace deps as inputs and .next/ as the declared output, plus a NextBuildInfo provider so downstream rules (deploy targets, oci_image wrappers, doc extractors) can consume builds.

← All modules