|
| 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 | + ) |
0 commit comments