8000 feat: add contrib/nextjs rules for building and running nextjs applic… · aspect-build/rules_js@0f1eb19 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0f1eb19

Browse files
committed
feat: add contrib/nextjs rules for building and running nextjs applications
1 parent 0b21e0b commit 0f1eb19

File tree

24 files changed

+1295
-8
lines changed

24 files changed

+1295
-8
lines changed

.aspect/rules/external_repository_action_cache/npm_translate_lock_LTE4Nzc1MDcwNjU=

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ examples/linked_empty_node_modules/package.json=-1039372825
1010
examples/linked_lib/package.json=1590632845
1111
examples/linked_pkg/package.json=-726181961
1212
examples/macro/package.json=857146175
13+
examples/nextjs/package.json=-16321579
1314
examples/npm_deps/package.json=-929156430
1415
examples/npm_deps/patches/meaning-of-life@1.0.0-pnpm.patch=-442666336
1516
examples/npm_package/libs/lib_a/package.json=-1377103079
@@ -31,5 +32,5 @@ npm/private/test/vendored/is-odd/package.json=1041695223
3132
npm/private/test/vendored/lodash-4.17.21.tgz=-1206623349
3233
npm/private/test/vendored/semver-max/package.json=578664053
3334
package.json=1510979981
34-
pnpm-lock.yaml=-349512393
35+
pnpm-lock.yaml=-553986311
3536
pnpm-workspace.yaml=854106668

.bazelignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ examples/linked_empty_node_modules/node_modules
77
examples/linked_lib/node_modules
88
examples/linked_pkg/node_modules
99
examples/macro/node_modules/
10+
examples/nextjs/node_modules/
1011
examples/npm_deps/node_modules/
1112
examples/npm_package/libs/lib_a/node_modules/
1213
examples/npm_package/packages/pkg_a/node_modules/

contrib/nextjs/BUILD.bazel

Whitespace-only changes.

contrib/nextjs/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# NextJs Bazel Utils
2+
3+
rules_js/contrib/nextjs is a set of rules for building and serving Next.js applications with Bazel.

contrib/nextjs/defs.bzl

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
"""Utilities for building Next.js applications with Bazel including building, running dev and prod servers, and creating OCI images.
2+
3+
This might be upstreamed to a "contrib" folder in rules_js at some point.
4+
Follow https://github.com/aspect-build/rules_js/issues/1292
5+
6+
Inspirect by: https://github.com/bazelbuild/examples/blob/7a5d4901e433be99cad1eba35afc62551a6136d4/frontend/next.js/defs.bzl
7+
"""
8+
9+
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
10+
load("@aspect_bazel_lib//lib:directory_path.bzl", "directory_path")
11+
load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_run_binary", "js_run_devserver")
12+
13+
# The nextjs output directory.
14+
# Changing this seems to break the 'next build' outputs so it is not configurable.
15+
_next_build_out = ".next"
16+
17+
# The nextjs config file.
18+
# Changing this seems to break the 'next build' outputs so it is not configurable.
19+
# _next_build_config = "next.config.mjs"
20+
21+
def nextjs(
22+
name,
23+
config,
24+
srcs,
25+
next_js_binary,
26+
next_bin,
27+
data = [],
28+
serve_data = [],
29+
**kwargs):
30+
"""Generates Next.js targets build, dev & start targets.
31+
32+
`{name}` - a nextjs production bundle
33+
`{name}.dev` - a nextjs devserver
34+
`{name}.start` - a nextjs prodserver
35+
`{name}.load` - an OCI image for running the production application in a container
36+
37+
Use this macro in the BUILD file at the root of a next app where the `next.config.js` file is
38+
located.
39+
40+
For example, a target such as
41+
42+
```
43+
next(
44+
name = "next",
45+
config = "next.config.js",
46+
srcs = [
47+
"//next.js/pages",
48+
"//next.js/public",
49+
"//next.js/styles",
50+
],
51+
data = [
52+
"//next.js:node_modules/next",
53+
"//next.js:node_modules/react-dom",
54+
"//next.js:node_modules/react",
55+
"//next.js:node_modules/typescript",
56+
"package.json",
57+
],
58+
next_bin = "../../node_modules/.bin/next",
59+
next_js_binary = "//:next_js_binary",
60+
)
61+
```
62+
63+
in an `next.js/BUILD.bazel` file where the root `BUILD.bazel` file has
64+
next linked to `node_modules` and the `next_js_binary`:
65+
66+
```
67+
load("@npm//:defs.bzl", "npm_link_all_packages")
68+
load("@npm//:next/package_json.bzl", next_bin = "bin")
69+
70+
npm_link_all_packages(name = "node_modules")
71+
72+
next_bin.next_binary(
73+
name = "next_js_binary",
74+
visibility = ["//visibility:public"],
75+
)
76+
```
77+
78+
will create the targets:
79+
80+
```
81+
//next.js:next
82+
//next.js:next.dev
83+
//next.js:next.start
84+
```
85+
86+
To build the above next app, equivalent to running
87+
`next build` outside Bazel, run,
88+
89+
```
90+
bazel build //next.js:next
91+
```
92+
93+
To run the development server in watch mode with
94+
[ibazel](https://github.com/bazelbuild/bazel-watcher), equivalent to running
95+
`next dev` outside Bazel, run
96+
97+
```
98+
ibazel run //next.js:next.dev
99+
```
100+
101+
To run the production server in watch mode with
102+
[ibazel](https://github.com/bazelbuild/bazel-watcher), equivalent to running
103+
`next start` outside Bazel,
104+
105+
```
106+
ibazel run //next.js:next.start
107+
```
108+
109+
Args:
110+
name: The name of the build target.
111+
112+
config: the nextjs config file. Typically `next.config.js`.
113+
114+
srcs: Source files to include in build & dev targets.
115+
Typically these are source files or transpiled source files in Next.js source folders
116+
such as `pages`, `public` & `styles`.
117+
118+
data: Data f 57AE iles to include in all targets.
119+
These are typically npm packages required for the build & configuration files such as
120+
package.json and next.config.js.
121+
122+
serve_data: Data files to include in devserver targets
123+
124+
next_js_binary: The next js_binary. Used for the `build `target.
125+
126+
Typically this is a js_binary target created using `bin` loaded from the `package_json.bzl`
127+
file of the npm package.
128+
129+
See main docstring above for example usage.
130+
131+
next_bin: The next bin command. Used for the `dev` and `start` targets.
132+
133+
Typically the path to the next entry point from the current package. For example `./node_modules/.bin/next`,
134+
if next is linked to the current package, or a relative path such as `../node_modules/.bin/next`, if next is
135+
linked in the parent package.
136+
137+
See main docstring above for example usage.
138+
139+
**kwargs: Other attributes passed to all targets such as `tags`.
140+
"""
141+
142+
next_standalone_binary(
143+
name = "{}.standalone".format(name),
144+
pkg = kwargs.pop("pkg", native.package_name()),
145+
app = ":{}".format(name),
146+
**kwargs
147+
)
148+
149+
# ---------------------------------------------------------------------------------------------
150+
# Building a bundle.
151+
152+
# `next build` creates an optimized bundle of the application
153+
# https://nextjs.org/docs/api-reference/cli#build
154+
js_run_binary(
155+
name = name,
156+
tool = next_js_binary,
157+
args = ["build"],
158+
srcs = srcs + data + [config],
159+
out_dirs = [_next_build_out],
160+
chdir = native.package_name(),
161+
mnemonic = "NextJs",
162+
progress_message = "Compile Next.js app %{label}",
163+
**kwargs
164+
)
165+
166+
# `next dev` runs the application in development mode
167+
# https://nextjs.org/docs/api-reference/cli#development
168+
js_run_devserver(
169+
name = "{}.dev".format(name),
170+
command = next_bin,
171+
args = ["dev"],
172+
data = srcs + data + [config] + serve_data,
173+
chdir = native.package_name(),
174+
**kwargs
175+
)
176+
177+
# `next start` runs the application in production mode
178+
# https://nextjs.org/docs/api-reference/cli#production
179+
js_run_devserver(
180+
name = "{}.start".format(name),
181+
command = next_bin,
182+
args = ["start"],
183+
data = data + [name, config] + serve_data,
184+
chdir = native.package_name(),
185+
**kwargs
186+
)
187+
188+
# ---------------------------------------------------------------------------------------------
189+
# A standalone binary for running the production application
190+
# ---------------------------------------------------------------------------------------------
191+
192+
def next_standalone_binary(name, pkg, app, deps = [], **kwargs):
193+
"""A `js_binary` to run a pre-compiled standalone Next.js application.
194+
195+
Args:
196+
name: the name of the standalone application binary target
197+
app: the pre-compiled standalone application directory via `next build` with `output: 'standalone'`
198+
deps: dependencies of the standalone application
199+
pkg: TODO
200+
**kwargs: other attributes passed to the `js_binary` target such as `visibility`.
201+
"""
202+
203+
# The output directory containing the standalone application.
204+
standalone_outdir = "%s-standalone" % name
205+
206+
# The server binary and required environment
207+
js_binary(
208+
name = name,
209+
entry_point = ":{}.js".format(name),
210+
chdir = native.package_name(),
211+
data = deps,
212+
**kwargs
213+
)
214+
215+
# The server entry point into the standalone directory.
216+
directory_path(
217+
name = "{}.js".format(name),
218+
directory = ":{}.standalone".format(name),
219+
path = "standalone/{}/server.js".format(pkg),
220+
visibility = ["//visibility:private"],
221+
tags = ["manual"],
222+
)
223+
224+
# Copy the standalone folder build and public/static to create a standalone server.
225+
# See https://nextjs.org/docs/pages/api-reference/config/next-config-js/output#automatically-copying-traced-files
226+
copy_to_directory(
227+
name = "{}.standalone".format(name),
228+
srcs = [app] + native.glob(["public/**"]),
229+
include_srcs_patterns = [
230+
"public/**",
231+
"{}/static/**".format(_next_build_out),
232+
"{}/standalone/**".format(_next_build_out),
233+
],
234+
exclude_srcs_patterns = [
235+
# TODO: exclude non-deterministic and log/trace files?
236+
# NOTE: the next.config.mjs should have already exclude the node_modules
237+
],
238+
replace_prefixes = {
239+
"{}/standalone".format(_next_build_out): "standalone",
240+
"{}/static".format(_next_build_out): "standalone/{}/{}/static".format(pkg, _next_build_out),
241+
"public": "standalone/{}/public".format(pkg),
242+
},
243+
out = standalone_outdir,
244+
visibility = ["//visibility:private"],
245+
tags = ["manual"],
246+
)

contrib/nextjs/next.bazel.mjs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { dirname, join } from 'node:path'
2+
import { fileURLToPath } from 'node:url'
3+
import { rmdirSync } from 'node:fs'
4+
5+
const appDir = import.meta.dirname || dirname(fileURLToPath(import.meta.url))
6+
const outDir = join(appDir, '.next')
7+
8+
/** @type {import('next').NextConfig} */
9+
const nextConfig = {
10+
output: 'standalone',
11+
12+
// TODO: limit the tracing directories more?
13+
// outputFileTracingExcludes - TODO: instead of removing standalone/node_modules later?
14+
// outputFileTracingIncludes
15+
16+
// TODO: try to prevent bazel sandbox directories in the output
17+
}
18+
19+
/**
20+
* NextJs within bazel copies node_modules symlinks pointing into .aspect_rules_js within the sandbox.
21+
* Clear all `standalone/node_modules` and assume the bazel rule will include the necessary npm packages.
22+
*/
23+
function nextjsFixSymlinks() {
24+
log(`Removing standalone/node_modules symlinks`)
25+
26+
rmdirSync(join(outDir, 'standalone/node_modules'), { recursive: true })
27+
28+
// TODO: try removing logging and unnecessary+non-deterministic outputs
29+
}
30+
31+
function log(...args) {
32+
console.log('[NextJs Bazel]: ', ...args)
33+
}
34+
35+
// Run the symlinks fixes on exit.
36+
process.on('exit', nextjsFixSymlinks)
37+
38+
export default nextConfig

examples/nextjs/BUILD.bazel

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
load("@aspect_rules_js//contrib/nextjs:defs.bzl", "nextjs")
2+
load("@npm//:defs.bzl", "npm_link_all_packages")
3+
load("@npm//examples/nextjs:next/package_json.bzl", next_bin = "bin")
4+
5+
npm_link_all_packages(name = "node_modules")
6+
7+
# The nextjs binary target used by nextjs.bzl
8+
next_bin.next_binary(
9+
name = "next_js_binary",
10+
11+
# Nextjs requires a modern node toolchain
12+
node_toolchain = select({
13+
"@bazel_tools//src/conditions:linux_x86_64": "@node20_linux_amd64//:node_toolchain",
14+
"@bazel_tools//src/conditions:darwin": "@node20_darwin_amd64//:node_toolchain",
15+
"@bazel_tools//src/conditions:windows": "@node20_windows_amd64//:node_toolchain",
16+
}),
17+
visibility = ["//visibility:public"],
18+
)
19+
20+
nextjs(
21+
name = "app",
22+
srcs = glob(["src/**/*"]),
23+
config = "next.config.mjs",
24+
25+
# The nextjs .bin and bazel binary target to use
26+
next_bin = "node_modules/.bin/next",
27+
next_js_binary = ":next_js_binary",
28+
)

examples/nextjs/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This is a basic [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app).
2+
3+
`@aspect_rules_js//contrib/nextjs:defs.bzl` is used to build+serve the Next.js app in Bazel.

examples/nextjs/jsconfig.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"paths": {
4+
"@/*": ["./src/*"]
5+
}
6+
}
7+
}

examples/nextjs/next.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
reactStrictMode: true,
4+
}
5+
6+
export default nextConfig

0 commit comments

Comments
 (0)
0