Skip to content

Commit 35d2106

Browse files
fix(nextjs): match standalone server copy patterns to build output la… (#2887)
Fix `nextjs_standalone_server` producing a `js_binary` that fails at runtime because `server.js` is never copied into the assembled standalone directory. ### Symptom An OCI image (or `bazel run`) built from `nextjs_standalone_build` + `nextjs_standalone_server` fails to start: ``` FATAL: aspect_rules_js[js_binary]: the entry_point '.../next_server.runfiles/_main/<pkg>/next_server/standalone/<pkg>/server.js' not found ``` The build succeeds — the failure only appears at runtime, because the server's `js_binary` entry point points at a `server.js` that was never assembled into the standalone output. ### Root cause `nextjs_standalone_server`'s internal `copy_to_directory` uses `include_srcs_patterns` / `replace_prefixes` prefixed with `.next/`: ```python include_srcs_patterns = [ "public/**", "{}/static/**".format(_next_build_out), # ".next/static/**" "{}/standalone/**".format(_next_build_out), # ".next/standalone/**" ], ``` But the `app` tree (the `nextjs_standalone_build` output) does **not** expose its files under a `.next/` prefix. `nextjs_standalone_build` runs `next build` (which writes to `.next`) and then copies that tree into a directory named after the **build target** via `_copy_exec_to_bin` — it cannot reuse `.next`, since that path is already owned by the build action. So a build target `:foo` yields paths like `foo/standalone/...` and `foo/static/...`. Since `copy_to_directory` matches paths relative to the package (default `root_paths = ["."]`), the `.next/`-prefixed patterns match nothing — only `public/**` is copied. `server.js` and `.next/static` never make it into the standalone directory, and the binary dies at runtime. Empirically confirmed (build target named `next`, package `displayads/panel/frontend`): - `.next/standalone/**` → matches nothing - `standalone/**` → matches nothing - `next/standalone/**` → matches (the prefix is the build target's directory name) This is a regression from #2879, which flattened the `.next` wrapper in `nextjs_standalone_build`'s output layout without updating `nextjs_standalone_server`'s patterns to match. ### Fix Derive the prefix from the `app` label (`app.split(":")[-1].split("/")[-1]`) and use it in `include_srcs_patterns` / `replace_prefixes` instead of the hardcoded `.next`, matching the actual flattened layout. This mirrors the existing label-parsing the macro already does for the config basename, and is independent of the Next.js version. ### Reproduction 1. `nextjs_standalone_build(name = "next", ...)` then `nextjs_standalone_server(name = "next_server", app = ":next")`. 2. `bazel build //path:_next_server.standalone`. 3. Inspect the output: only `public/` is present; no `server.js`, no `.next/static`. 4. Running the resulting `js_binary` → `entry_point ... server.js not found`. --- ### Changes are visible to end-users: yes - Searched for relevant documentation and updated as needed: no - Breaking change (forces users to change their own code or config): no - Suggested release notes appear below: yes > `nextjs_standalone_server`: fix the assembled standalone directory missing `server.js` and `.next/static`, which caused the server `js_binary` to fail at runtime with "entry_point ... server.js not found". Regression from #2879. ### Test plan - Manual testing; to reproduce: 1. `nextjs_standalone_build(name = "next", ...)` + `nextjs_standalone_server(name = "next_server", app = ":next")`. 2. `bazel build //path:_next_server.standalone` and inspect the output — `standalone/<pkg>/server.js` and `standalone/<pkg>/.next/static` are now present. 3. `bazel run //path:next_server` starts successfully instead of failing with the missing-entry-point error.
1 parent 72e667a commit 35d2106

1 file changed

Lines changed: 16 additions & 4 deletions

File tree

contrib/nextjs/defs.bzl

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,18 @@ def nextjs_standalone_server(name, app, pkg = None, data = [], **kwargs):
393393
if pkg == None:
394394
pkg = native.package_name()
395395

396+
# The `app` tree artifact (from `nextjs_standalone_build`) exposes its files under a directory
397+
# named after the build target rather than under a `.next/` prefix. `nextjs_standalone_build`
398+
# runs `next build` (which writes to `.next`) and then copies that tree into a directory named
399+
# after the build target via `_copy_exec_to_bin` (it cannot reuse `.next`, which is already
400+
# owned by the build action). So a build target `:foo` yields paths like `foo/standalone/...`,
401+
# `foo/static/...`. Derive that prefix from the `app` label to match the actual layout.
402+
#
403+
# Strip only the package portion (everything up to and including the last `:`); Bazel target
404+
# names may legitimately contain slashes (e.g. `:web/next`), and those slashes are part of the
405+
# directory name, so they must be preserved.
406+
app_dir = app.split(":")[-1] if ":" in app else app.split("/")[-1]
407+
396408
# The standalone server binary
397409
js_binary(
398410
name = name,
@@ -418,15 +430,15 @@ def nextjs_standalone_server(name, app, pkg = None, data = [], **kwargs):
418430
srcs = [app] + native.glob(["public/**"]),
419431
include_srcs_patterns = [
420432
"public/**",
421-
"{}/static/**".format(_next_build_out),
422-
"{}/standalone/**".format(_next_build_out),
433+
"{}/static/**".format(app_dir),
434+
"{}/standalone/**".format(app_dir),
423435
],
424436
exclude_srcs_patterns = [
425437
# TODO: exclude non-deterministic and log/trace files?
426438
],
427439
replace_prefixes = {
428-
"{}/standalone".format(_next_build_out): "standalone",
429-
"{}/static".format(_next_build_out): "standalone/{}/{}/static".format(pkg, _next_build_out),
440+
"{}/standalone".format(app_dir): "standalone",
441+
"{}/static".format(app_dir): "standalone/{}/{}/static".format(pkg, _next_build_out),
430442
"public": "standalone/{}/public".format(pkg),
431443
},
432444
out = standalone_outdir,

0 commit comments

Comments
 (0)