Release v3.8.9 — final sync (static-asset fix + contributor credits) - #3135
Conversation
…teral .next
CRITICAL white-screen bug from the build-output-isolation refactor: the standalone
server.js bakes distDir ("./.build/next") into its config and serves /_next/static
from <root>/.build/next/static — but assembleStandalone hard-coded the destination
to <outDir>/.next/static (+ sanitised/patched <outDir>/.next/{required-server-files,
server}). Result: the server's static dir was EMPTY → every JS/CSS chunk 404'd →
blank login page (health stayed 200, so it slipped past the health-only dry-run).
Mirror the distDir path (relative to projectRoot) for static, required-server-files
sanitization (was a silent no-op → 0 paths sanitised, now 11), and the Turbopack
chunk patch. Verified: booting the assembled bundle serves the webpack chunk 200.
Affects every consumer (npm/Docker/Electron/VPS).
…ic-asset build fix - #3131 Kiro Opus 4.8 catalog (thanks @artickc) - #3132 Kimi thinking-mode reasoning_content fix (thanks @bypanghu) - #3130 connectionId fallback + kilo call logging (thanks @androw) - build: standalone static-asset path fix (white login screen after build-output reorg) - contributors hall: +@artickc +@bypanghu +@androw (15 total)
There was a problem hiding this comment.
Code Review
This pull request updates the standalone build assembly process to support custom build output directories (distDir) instead of assuming a hardcoded ".next" directory. It ensures static assets, required server files, and Turbopack chunks are correctly placed and referenced. The review feedback identifies edge cases where path resolution could fail if distDir is absolute or if the project root differs from the current working directory, offering robust code suggestions to normalize these paths.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function assemblePathSanitize(projectRoot, outDir, distDir = ".next") { | ||
| const buildRoot = projectRoot.replace(/\\/g, "/"); // normalise for regex safety | ||
| const sanitizeTargets = [ | ||
| path.join(outDir, "server.js"), | ||
| path.join(outDir, ".next", "required-server-files.json"), | ||
| // required-server-files.json lives under the distDir (e.g. .build/next), not | ||
| // a literal .next — the standalone preserves the configured distDir path. | ||
| path.join(outDir, distDir, "required-server-files.json"), | ||
| ]; |
There was a problem hiding this comment.
If distDir is passed as an absolute path, path.join(outDir, distDir, ...) will produce an incorrect path (e.g., outDir/absolute/path/...), causing fsSync.existsSync to fail and silently skipping the sanitization of required-server-files.json.
Since assemblePathSanitize has access to projectRoot, we can defensively normalize distDir to be relative to projectRoot if it is absolute.
| export function assemblePathSanitize(projectRoot, outDir, distDir = ".next") { | |
| const buildRoot = projectRoot.replace(/\\/g, "/"); // normalise for regex safety | |
| const sanitizeTargets = [ | |
| path.join(outDir, "server.js"), | |
| path.join(outDir, ".next", "required-server-files.json"), | |
| // required-server-files.json lives under the distDir (e.g. .build/next), not | |
| // a literal .next — the standalone preserves the configured distDir path. | |
| path.join(outDir, distDir, "required-server-files.json"), | |
| ]; | |
| export function assemblePathSanitize(projectRoot, outDir, distDir = ".next") { | |
| const buildRoot = projectRoot.replace(/\\/g, "/"); // normalise for regex safety | |
| const relDistDir = path.isAbsolute(distDir) ? path.relative(projectRoot, distDir) : distDir; | |
| const sanitizeTargets = [ | |
| path.join(outDir, "server.js"), | |
| // required-server-files.json lives under the distDir (e.g. .build/next), not | |
| // a literal .next — the standalone preserves the configured distDir path. | |
| path.join(outDir, relDistDir, "required-server-files.json"), | |
| ]; |
| // The standalone bundle preserves the distDir path RELATIVE to projectRoot | ||
| // (the server's baked config uses e.g. "./.build/next"), so output dest paths | ||
| // for static / required-server-files / server chunks must use the relative | ||
| // distDir appended to outDir — never the absolute build-machine distDir. | ||
| const relDistDir = path.isAbsolute(distDir) ? path.relative(projectRoot, distDir) : distDir; | ||
|
|
||
| const standaloneDir = path.resolve(path.join(distDir, "standalone")); |
There was a problem hiding this comment.
If distDir is relative and projectRoot is different from process.cwd(), path.resolve(path.join(distDir, "standalone")) will resolve against process.cwd() instead of projectRoot. This can lead to incorrect path resolution in monorepos or custom build environments.
We should resolve distDir against projectRoot first if it is relative, and then derive relDistDir and standaloneDir from it.
| // The standalone bundle preserves the distDir path RELATIVE to projectRoot | |
| // (the server's baked config uses e.g. "./.build/next"), so output dest paths | |
| // for static / required-server-files / server chunks must use the relative | |
| // distDir appended to outDir — never the absolute build-machine distDir. | |
| const relDistDir = path.isAbsolute(distDir) ? path.relative(projectRoot, distDir) : distDir; | |
| const standaloneDir = path.resolve(path.join(distDir, "standalone")); | |
| // The standalone bundle preserves the distDir path RELATIVE to projectRoot | |
| // (the server's baked config uses e.g. "./.build/next"), so output dest paths | |
| // for static / required-server-files / server chunks must use the relative | |
| // distDir appended to outDir — never the absolute build-machine distDir. | |
| const resolvedDistDir = path.isAbsolute(distDir) ? distDir : path.resolve(projectRoot, distDir); | |
| const relDistDir = path.relative(projectRoot, resolvedDistDir); | |
| const standaloneDir = path.join(resolvedDistDir, "standalone"); |
| const staticSrc = path.join(distDir, "static"); | ||
| const staticDest = path.join(resolvedOutDir, ".next", "static"); | ||
| const staticDest = path.join(resolvedOutDir, relDistDir, "static"); |
There was a problem hiding this comment.
To ensure correct path resolution when projectRoot is different from process.cwd() and distDir is relative, staticSrc should be resolved using resolvedDistDir instead of distDir.
| const staticSrc = path.join(distDir, "static"); | |
| const staticDest = path.join(resolvedOutDir, ".next", "static"); | |
| const staticDest = path.join(resolvedOutDir, relDistDir, "static"); | |
| const staticSrc = path.join(resolvedDistDir, "static"); | |
| const staticDest = path.join(resolvedOutDir, relDistDir, "static"); |
CI Coverage Report
Coverage artifact was not available for this run. |
…rCloud gate) Extract patchStandalonePackageJson / copyStaticAndPublic / copyNativeAssetsAndExtraModules helpers so assembleStandalone drops from cognitive complexity 29 → ~12 (≤15 gate). Also: replaceAll over replace, String.raw for the regex-escape replacement (2 minor smells). Pure refactor — assemble-standalone.test.ts still green; no behavior change.
|
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (3 files)
Reviewed by nemotron-3-super-120b-a12b-20230311:free · 1,845,266 tokens |
Release v3.8.9 — final sync (static-asset fix + contributor credits)
Release v3.8.9 — final sync (static-asset fix + contributor credits)
Release v3.8.9 — final sync (static-asset fix + contributor credits)



Finalizes the v3.8.9 release for tagging. Brings the two commits that landed on `release/v3.8.9` after the initial release merge (#3092):
Included
Validation
After merge → tag `v3.8.9` → npm/docker/electron publishes.