blob: 1a67768bf224f6dc0fd3cfd3bd586b049997ebc7 [file] [log] [blame]
"""Utilities for building Next.js applications with Bazel and rules_js.
All invocations of Next.js are done through a `next_js_binary` target passed into the macros.
This is normally generated once alongside the `package.json` containing the `next` dependency:
```
load("@npm//:next/package_json.bzl", next_bin = "bin")
next_bin.next_binary(
name = "next_js_binary",
visibility = ["//visibility:public"],
)
```
The next binary is then passed into the macros, for example:
```
nextjs_build(
name = "next",
config = "next.config.mjs",
srcs = glob(["src/**"]),
next_js_binary = "//:next_js_binary",
)
```
# Macros
There are two sets of macros for building Next.js applications: standard and standalone.
## Standard
- `nextjs()`: wrap the build+dev+start targets
- `nextjs_build()`: the Next.js [build](https://nextjs.org/docs/app/building-your-application/deploying#production-builds) command
- `nextjs_dev()`: the Next.js [dev](https://nextjs.org/docs/app/getting-started/installation#run-the-development-server) command
- `nextjs_start()`: the Next.js [start](https://nextjs.org/docs/app/building-your-application/deploying#nodejs-server) command,
accepting a Next.js build artifact to start
## Standalone
For [standalone applications](https://nextjs.org/docs/app/api-reference/config/next-config-js/output#automatically-copying-traced-files):
- `nextjs_standalone_build()`: the Next.js [build](https://nextjs.org/docs/app/building-your-application/deploying#production-builds) command,
configured for a standalone application within bazel
- `nextjs_standalone_server()`: constructs a standalone Next.js server `js_binary` following the
[standalone directory structure guidelines](https://nextjs.org/docs/app/api-reference/config/next-config-js/output#automatically-copying-traced-files)
"""
load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file")
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
load("@aspect_bazel_lib//lib:directory_path.bzl", "directory_path")
load("//js:defs.bzl", "js_binary", "js_run_binary", "js_run_devserver")
# The Next.js output directory which is not configurable
_next_build_out = ".next"
# The Next.js config file which is not configurable (other then file extension).
_next_build_config = "next.config.mjs"
# A label to the rules_js/contrib config file.
_next_standalone_config = Label("next.bazel.mjs")
def nextjs(
name,
srcs,
next_js_binary,
config = "next.config.mjs",
data = [],
serve_data = [],
**kwargs):
"""Generates Next.js build, dev & start targets.
`{name}` - a Next.js production bundle
`{name}.dev` - a Next.js devserver
`{name}.start` - a Next.js prodserver
Use this macro in the BUILD file at the root of a next app where the `next.config.mjs`
file is located.
For example, a target such as `//app:next` in `app/BUILD.bazel`
```
next(
name = "next",
config = "next.config.mjs",
srcs = glob(["src/**"]),
data = [
"//:node_modules/next",
"//:node_modules/react-dom",
"//:node_modules/react",
"package.json",
],
next_js_binary = "//:next_js_binary",
)
```
will create the targets:
```
//app:next
//app:next.dev
//app:next.start
```
To build the above next app, equivalent to running `next build` outside Bazel:
```
bazel build //app:next
```
To run the development server in watch mode with
[ibazel](https://github.com/bazelbuild/bazel-watcher), equivalent to running
`next dev` outside Bazel:
```
ibazel run //app:next.dev
```
To run the production server in watch mode with
[ibazel](https://github.com/bazelbuild/bazel-watcher), equivalent to running
`next start` outside Bazel:
```
ibazel run //app:next.start
```
Args:
name: the name of the build target
config: the Next.js config file. Typically `next.config.mjs`.
srcs: Source files to include in build & dev targets.
Typically these are source files or transpiled source files in Next.js source folders
such as `pages`, `public` & `styles`.
data: Data files to include in all targets.
These are typically npm packages required for the build & configuration files such as
package.json and next.config.js.
serve_data: Data files to include in devserver targets
next_js_binary: The next `js_binary` target to use for running Next.js
Typically this is a js_binary target created using `bin` loaded from the `package_json.bzl`
file of the npm package.
See main docstring above for example usage.
**kwargs: Other attributes passed to all targets such as `tags`.
"""
nextjs_build(
name = name,
config = config,
srcs = srcs,
next_js_binary = next_js_binary,
data = data,
**kwargs
)
nextjs_dev(
name = name + ".dev",
config = config,
srcs = srcs,
data = data + serve_data,
next_js_binary = next_js_binary,
**kwargs
)
nextjs_start(
name = name + ".start",
config = config,
app = ":{}".format(name),
next_js_binary = next_js_binary,
# NOTE: must include `srcs` in addition to the pre-compiled `app` to include transitive
# deps such as npm packages, including the core Next.js and react packages. Could possibly
# remove source files and only include transitive deps.
data = data + srcs + serve_data,
**kwargs
)
def nextjs_build(name, config, srcs, next_js_binary, data = [], **kwargs):
"""Build the Next.js production artifact.
See https://nextjs.org/docs/pages/api-reference/cli/next#build
Args:
name: the name of the build target
config: the Next.js config file
srcs: the sources to include in the build, including any transitive deps
data: the data files to include in the build
next_js_binary: The next `js_binary` target to use for running Next.js
Typically this is a js_binary target created using `bin` loaded from the `package_json.bzl`
file of the npm package.
See main docstring above for example usage.
**kwargs: Other attributes passed to all targets such as `tags`, env
"""
js_run_binary(
name = name,
tool = next_js_binary,
args = ["build"],
srcs = srcs + data + [config],
out_dirs = [_next_build_out],
chdir = native.package_name(),
mnemonic = "NextJs",
progress_message = "Compile Next.js app %{label}",
**kwargs
)
def nextjs_start(name, config, app, next_js_binary, data = [], **kwargs):
"""Run the Next.js production server for an app.
See https://nextjs.org/docs/pages/api-reference/cli/next#next-start-options
Args:
name: the name of the build target
config: the Next.js config file
app: the pre-compiled Next.js application, typically the output of `nextjs_build`
data: additional server data
next_js_binary: The next `js_binary` target to use for running Next.js
Typically this is a js_binary target created using `bin` loaded from the `package_json.bzl`
file of the npm package.
See main docstring above for example usage.
**kwargs: Other attributes passed to all targets such as `tags`, env
"""
js_run_devserver(
name = name,
tool = next_js_binary,
args = ["start"],
data = data + [app, config],
chdir = native.package_name(),
**kwargs
)
def nextjs_dev(name, config, srcs, data, next_js_binary, **kwargs):
"""Run the Next.js development server.
See https://nextjs.org/docs/pages/api-reference/cli/next#next-dev-options
Args:
name: the name of the build target
config: the Next.js config file
srcs: the sources to include in the build, including any transitive deps
data: additional devserver runtime data
next_js_binary: The next `js_binary` target to use for running Next.js
Typically this is a js_binary target created using `bin` loaded from the `package_json.bzl`
file of the npm package.
See main docstring above for example usage.
**kwargs: Other attributes passed to all targets such as `tags`, env
"""
js_run_devserver(
name = name,
tool = next_js_binary,
args = ["dev"],
data = srcs + data + [config],
chdir = native.package_name(),
**kwargs
)
# ---------------------------------------------------------------------------------------------
# Standalone Next.js build & server binary.
# ---------------------------------------------------------------------------------------------
def nextjs_standalone_build(name, config, srcs, next_js_binary, data = [], **kwargs):
"""Compile a standalone Next.js application.
See https://nextjs.org/docs/app/api-reference/config/next-config-js/output#automatically-copying-traced-files
NOTE: a `next.config.mjs` is generated, wrapping the passed `config`, to overcome Next.js limitation with bazel,
rules_js and pnpm (with hoist=false, as required by rules_js).
Due to the generated `next.config.mjs` file the `nextjs_standalone_build(config)` must have a unique name
or file path that does not conflict with standard Next.js config files.
Issues worked around by the generated config include:
* https://github.com/vercel/next.js/issues/48017
* https://github.com/aspect-build/rules_js/issues/714
Args:
name: the name of the build target
config: the Next.js config file
srcs: the sources to include in the build, including any transitive deps
next_js_binary: the Next.js binary to use for building
data: the data files to include in the build
**kwargs: Other attributes passed to all targets such as `tags`, env
"""
# Wrap the config file to add necessary bazel logic
env = kwargs.pop("env", {})
env["NEXTJS_STANDALONE_CONFIG"] = "$(locations %s)" % config
copy_file(
name = "_%s.standalone_config_file" % name,
src = _next_standalone_config,
out = _next_build_config,
visibility = ["//visibility:private"],
tags = ["manual"],
)
# `next build` of the standalone application
js_run_binary(
name = name,
tool = next_js_binary,
env = env,
args = ["build"],
srcs = srcs + data + [":_%s.standalone_config_file" % name, config],
out_dirs = [_next_build_out],
chdir = native.package_name(),
mnemonic = "NextJs",
progress_message = "Compile Next.js standalone app %{label}",
**kwargs
)
def nextjs_standalone_server(name, app, pkg = None, data = [], **kwargs):
"""Configures the output of a standalone Next.js application to be a standalone server binary.
See the Next.js [standalone server documentation](https://nextjs.org/docs/app/api-reference/config/next-config-js/output#automatically-copying-traced-files)
for details on the standalone server directory structure.
This function is normally used in conjunction with `nextjs_standalone_build` to create a standalone
Next.js application. The standalone server is a `js_binary` target that can be run with `bazel run`
or deployed in a container image etc.
Args:
name: the name of the binary target
app: the standalone app directory, typically the output of `nextjs_standalone_build`
pkg: the directory server.js is in within the standalone/ directory.
This is normally the application path relative to the pnpm-lock.yaml.
Default: native.package_name() (for a pnpm-lock.yaml in the root of the workspace)
data: runtime data required to run the standalone server.
Normally requires `[":node_modules/next", ":node_modules/react"]` which are not included
in the Next.js standalone output.
**kwargs: additional `js_binary` attributes
"""
# The output directory containing the standalone application.
standalone_outdir = name
if pkg == None:
pkg = native.package_name()
# The standalone server binary
js_binary(
name = name,
entry_point = ":_{}.js".format(name),
chdir = native.package_name(),
data = data,
**kwargs
)
# The server entry point into the standalone directory.
directory_path(
name = "_{}.js".format(name),
directory = ":_{}.standalone".format(name),
path = "standalone/{}/server.js".format(pkg),
visibility = ["//visibility:private"],
tags = ["manual"],
)
# Copy the standalone directory and public/static to create a standalone server.
# See https://nextjs.org/docs/pages/api-reference/config/next-config-js/output#automatically-copying-traced-files
copy_to_directory(
name = "_{}.standalone".format(name),
srcs = [app] + native.glob(["public/**"]),
include_srcs_patterns = [
"public/**",
"{}/static/**".format(_next_build_out),
"{}/standalone/**".format(_next_build_out),
],
exclude_srcs_patterns = [
# TODO: exclude non-deterministic and log/trace files?
],
replace_prefixes = {
"{}/standalone".format(_next_build_out): "standalone",
"{}/static".format(_next_build_out): "standalone/{}/{}/static".format(pkg, _next_build_out),
"public": "standalone/{}/public".format(pkg),
},
out = standalone_outdir,
visibility = ["//visibility:private"],
tags = ["manual"],
)