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

Skip to content

Commit bd43409

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

File tree

24 files changed

+1355
-8
lines changed

24 files changed

+1355
-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+ A3E2
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: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
"""Utilities for building Next.js applications with Bazel and rules_js.
2+
"""
3+
4+
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
5+
load("@aspect_bazel_lib//lib:directory_path.bzl", "directory_path")
6+
load("//js:defs.bzl", "js_binary", "js_image_layer", "js_run_binary", "js_run_devserver")
7+
8+
# The nextjs output directory.
9+
# Changing this seems to break the 'next build' outputs so it is not configurable.
10+
_next_build_out = ".next"
11+
12+
# The nextjs config file.
13+
# Changing this seems to break the 'next build' outputs so it is not configurable.
14+
# _next_build_config = "next.config.mjs"
15+
16+
def nextjs(
17+
name,
18+
srcs,
19+
next_js_binary,
20+
next_bin,
21+
config = "next.config.mjs",
22+
data = [],
23+
serve_data = [],
24+
**kwargs):
25+
"""Generates Next.js build, dev & start targets.
26+
27+
`{name}` - a nextjs production bundle
28+
`{name}.dev` - a nextjs devserver
29+
`{name}.start` - a nextjs prodserver
30+
31+
Use this macro in the BUILD file at the root of a next app where the `next.config.mjs`
32+
file is located.
33+
34+
For example, a target such as `//app:next` in `app/BUILD.bazel`
35+
36+
```
37+
next(
38+
name = "next",
39+
config = "next.config.mjs",
40+
srcs = glob(["src/**"]),
41+
data = [
42+
"//:node_modules/next",
43+
"//:node_modules/react-dom",
44+
"//:node_modules/react",
45+
"package.json",
46+
],
47+
next_bin = "../node_modules/.bin/next",
48+
next_js_binary = "//:next_js_binary",
49+
)
50+
```
51+
52+
where the root `BUILD.bazel` file has next linked to `node_modules`
53+
and the `next_js_binary`:
54+
55+
```
56+
load("@npm//:defs.bzl", "npm_link_all_packages")
57+
load("@npm//:next/package_json.bzl", next_bin = "bin")
58+
59+
npm_link_all_packages(name = "node_modules")
60+
61+
next_bin.next_binary(
62+
name = "next_js_binary",
63+
visibility = ["//visibility:public"],
64+
)
65+
```
66+
67+
will create the targets:
68+
69+
```
70+
//app:next
71+
//app:next.dev
72+
//app:next.start
73+
```
74+
75+
To build the above next app, equivalent to running `next build` outside Bazel:
76+
77+
```
78+
bazel build //app:next
79+
```
80+
81+
To run the development server in watch mode with
82+
[ibazel](https://github.com/bazelbuild/bazel-watcher), equivalent to running
83+
`next dev` outside Bazel:
84+
85+
```
86+
ibazel run //app:next.dev
87+
```
88+
89+
To run the production server in watch mode with
90+
[ibazel](https://github.com/bazelbuild/bazel-watcher), equivalent to running
91+
`next start` outside Bazel:
92+
93+
```
94+
ibazel run //app:next.start
95+
```
96+
97+
Args:
98+
name: the name of the build target
99+
100+
config: the nextjs config file. Typically `next.config.mjs`.
101+
102+
srcs: Source files to include in build & dev targets.
103+
Typically these are source files or transpiled source files in Next.js source folders
104+
such as `pages`, `public` & `styles`.
105+
106+
data: Data files to include in all targets.
107+
These are typically npm packages required for the build & configuration files such as
108+
package.json and next.config.js.
109+
110+
serve_data: Data files to include in devserver targets
111+
112+
next_js_binary: The next js_binary. Used for the `build `target.
113+
114+
Typically this is a js_binary target created using `bin` loaded from the `package_json.bzl`
115+
file of the npm package.
116+
117+
See main docstring above for example usage.
118+
119+
next_bin: The next bin command. Used for the `dev` and `start` targets.
120+
121+
Typically the path to the next entry point from the current package. For example `./node_modules/.bin/next`,
122+
if next is linked to the current package, or a relative path such as `../node_modules/.bin/next`, if next is
123+
linked in the parent package.
124+
125+
See main docstring above for example usage.
126+
127+
**kwargs: Other attributes passed to all targets such as `tags`.
128+
"""
129+
130+
# TODO: must wrap `config` in another `next.config.mjs` file and `import next.bazel.mjs` to setup hooks
131+
132+
# `next build` creates an optimized bundle of the application
133+
# https://nextjs.org/docs/api-reference/cli#build
134+
js_run_binary(
135+
name = name,
136+
tool = next_js_binary,
137+
args = ["build"],
138+
srcs = srcs + data + [config],
139+
out_dirs = [_next_build_out],
140+
chdir = native.package_name(),
141+
mnemonic = "NextJs",
142+
progress_message = "Compile Next.js app %{label}",
143+
**kwargs
144+
)
145+
146+
# `next dev` runs the application in development mode
147+
# https://nextjs.org/docs/api-reference/cli#development
148+
js_run_devserver(
149+
name = "{}.dev".format(name),
150+
command = next_bin,
151+
args = ["dev"],
152+
data = srcs + data + [config] + serve_data,
153+
chdir = native.package_name(),
154+
**kwargs
155+
)
156+
157+
# `next start` runs the application in production mode
158+
# https://nextjs.org/docs/api-reference/cli#production
159+
js_run_devserver(
160+
name = "{}.start".format(name),
161+
command = next_bin,
162+
args = ["start"],
163+
data = data + [name, config] + serve_data,
164+
chdir = native.package_name(),
165+
**kwargs
166+
)
167+
168+
# ---------------------------------------------------------------------------------------------
169+
# A standalone binary for running the production application
170+
# ---------------------------------------------------------------------------------------------
171+
172+
def nextjs_standalone(name, app, deps = [], **kwargs):
173+
"""A `js_binary` to run a standalone Next.js application.
174+
175+
The `app` target must be a directory containing the output of a `next build` command with
176+
the `output: "standalone"` option set in the `next.config.mjs` file. For example, the output
177+
directory of the `nextjs()` macro.
178+
179+
Args:
180+
name: the name of the application binary target
181+
182+
app: the nextjs application directory which was compiled in with `output: "standalone"
183+
184+
This is typically the output of the `nextjs()` macro.
185+
186+
deps: dependencies required to run the standalone application
187+
188+
This would normally include the `:node_modules/next` and `:node_modules/react` packages
189+
that are not bundled into the standalone application.
190+
191+
This is separate from the `nextjs(deps)` required to bundle the application.
192+
193+
**kwargs: other attributes passed to the `js_binary` target such as `visibility`.
194+
"""
195+
196+
# The output directory containing the standalone application.
197+
# TODO: make it configurable in the macro?
198+
standalone_outdir = "%s-standalone" % name
199+
200+
# The subdirectory server.js is in within the standalone/ directory.
201+
# TODO: how to override this in nextjs config? Or need to make it configurable in the macro?
202+
pkg = native.package_name()
203+
204+
# The server binary and required environment
205+
js_binary(
206+
name = name,
207+
entry_point = ":_{}.js".format(name),
208+
chdir = native.package_name(),
209+
data = deps,
210+
**kwargs
211+
)
212+
213+
# The server entry point into the standalone directory.
214+
directory_path(
215+
name = "_{}.js".format(name),
216+
directory = ":_{}.standalone".format(name),
217+
path = "standalone/{}/server.js".format(pkg),
218+
visibility = ["//visibility:private"],
219+
tags = ["manual"],
220+
)
221+
222+
# Copy the standalone folder build and public/static to create a standalone server.
223+
# See https://nextjs.org/docs/pages/api-reference/config/next-config-js/output#automatically-copying-traced-files
224+
copy_to_directory(
225+
name = "_{}.standalone".format(name),
226+
srcs = [app] + native.glob(["public/**"]),
227+
include_srcs_patterns = [
228+
"public/**",
229+
"{}/static/**".format(_next_build_out),
230+
"{}/standalone/**".format(_next_build_out),
231+
],
232+
exclude_srcs_patterns = [
233+
# TODO: exclude non-deterministic and log/trace files?
234+
235+
# NOTE: the rules_js/contrib/nextjs config should have already excluded the node_modules
236+
],
237+
replace_prefixes = {
238+
"{}/standalone".format(_next_build_out): "standalone",
239+
"{}/static".format(_next_build_out): "standalone/{}/{}/static".format(pkg, _next_build_out),
240+
"public": "standalone/{}/public".format(pkg),
241+
},
242+
out = standalone_outdir,
243+
visibility = ["//visibility:private"],
244+
tags = ["manual"],
245+
)
246+
247+
# ---------------------------------------------------------------------------------------------
248+
# Next.js image layer - an optimal image layer for Next.js standalone applications.
249+
# ---------------------------------------------------------------------------------------------
250+
251+
def nextjs_standalone_image_layer(name, visibility, **kwargs):
252+
"""A `js_image_layer` optimized for a Next.js standalone application.
253+
254+
Args:
255+
name: The name of the build target.
256+
visibility: visibility of the main target
257+
**kwargs: Other attributes passed to the `js_image_layer` target such as `visibility`.
258+
"""
259+
js_image_layer(
260+
name = "_{}.layers".format(name),
261+
layer_groups = select({
262+
"@platforms//cpu:arm64": {
263+
"excluded_files": _excluded_package_files,
264+
"excluded_packages": _excluded_packages_arm,
265+
},
266+
"@platforms//cpu:x86_64": {
267+
"excluded_files": _excluded_package_files,
268+
"excluded_packages": _excluded_packages_amd,
269+
},
270+
}),
271+
visibility = ["//visibility:private"],
272+
**kwargs
273+
)
274+
275+
# Include all the standard layers, the exclude layers are not included.
276+
for layer in _DEFAULT_LAYER_GROUPS:
277+
native.filegroup(
278+
name = "_{}.{}.layer".format(name, layer),
279+
srcs = [":_{}.layers".format(name)],
280+
visibility = ["//visibility:private"],
281+
output_group = layer,
282+
)
283+
284+
native.filegroup(
285+
name = name,
286+
srcs = [":_{}.{}.layer".format(name, layer) for layer in _DEFAULT_LAYER_GROUPS],
287+
visibility = visibility,
288+
)
289+
290+
# The standard rules_js `js_image_layer` layer names.
291+
# See https://github.com/aspect-build/rules_js/blob/v2.3.3/docs/js_image_layer.md#performance for
292+
# the list and description.
293+
_DEFAULT_LAYER_GROUPS = ["node", "package_store_3p", "package_store_1p", "node_modules", "app"]
294+
295+
# Mimick the `node_modules` directory outputted by Next.js where the application is traced
296+
# and only the required files are included in the standalone application `node_modules`.
297+
#
298+
# The `excluded_*` layers involve no tracing and are simple regex patterns of excludes.
299+
# These exclude enough to create images with sizes similar to a non-bazel Next.js standalone build.
300+
301+
# Basic files that are easy to exclude.
302+
_excluded_package_files = "/node_modules/.*(%s)$" % "|".join([
303+
"\\.d\\.[cm]?ts",
304+
])
305+
306+
# Exclude platform specific packages that are not required in the image.
307+
# See https://github.com/aspect-build/rules_js/issues/2140 for potential future improvements.
308+
309+
# Large packages either unnecessary or unnecessary for the platform.
310+
_excluded_packages_arm = "/node_modules/(.*/)?((%s))" % ")|(".join([
311+
"@next[/+]swc(?!.*linux-arm64(-gnu)?@)",
312+
"@img[/+]sharp(?!.*linux-arm64)",
313+
])
314+
_excluded_packages_amd = "/node_modules/(.*/)?((%s))" % ")|(".join([
315+
"@next[/+]swc(?!.*linux-(amd64|x64)(-gnu)?)",
316+
"@img[/+]sharp(?!.*linux-(amd64|x64))",
317+
])

contrib/nextjs/next.bazel.mjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
/**
9+
* NextJs within bazel copies node_modules symlinks pointing into .aspect_rules_js within the sandbox.
10+
* Clear all `standalone/node_modules` and assume the bazel rule will include the necessary npm packages.
11+
*/
12+
function nextjsFixSymlinks() {
13+
log(`Removing standalone/node_modules symlinks`)
14+
15+
rmdirSync(join(outDir, 'standalone/node_modules'), { recursive: true })
16+
17+
// TODO: try removing logging and unnecessary+non-deterministic outputs
18+
}
19+
20+
function log(...args) {
21+
console.log('[NextJs Bazel]: ', ...args)
22+
}
23+
24+
// Run the symlinks fixes on exit.
25+
process.on('exit', nextjsFixSymlinks)

0 commit comments

Comments
 (0)
0