From 49dedecc42117bb9c552e31ac2b9d35d282cbcaf Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 4 Jun 2026 02:17:51 -0300 Subject: [PATCH] fix(build): assemble static/server-files/chunks under distDir, not literal .next MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 /.build/next/static — but assembleStandalone hard-coded the destination to /.next/static (+ sanitised/patched /.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). --- scripts/build/assembleStandalone.mjs | 28 ++++++++++++++------ tests/unit/build/assemble-standalone.test.ts | 11 +++++++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/scripts/build/assembleStandalone.mjs b/scripts/build/assembleStandalone.mjs index 275cf0219b..77e2231c5a 100644 --- a/scripts/build/assembleStandalone.mjs +++ b/scripts/build/assembleStandalone.mjs @@ -283,11 +283,13 @@ async function syncExtraModulesToDir(projectRoot, outDir, fsImpl, log) { * @param {string} outDir - assembled standalone output directory * @returns {number} number of path replacements made */ -export function assemblePathSanitize(projectRoot, outDir) { +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"), ]; let sanitisedCount = 0; @@ -316,8 +318,8 @@ export function assemblePathSanitize(projectRoot, outDir) { * @param {string} outDir - assembled standalone output directory * @returns {{ patchedFiles: number, patchedMatches: number }} */ -export function patchTurbopackChunks(outDir) { - const serverOutput = path.join(outDir, ".next", "server"); +export function patchTurbopackChunks(outDir, distDir = ".next") { + const serverOutput = path.join(outDir, distDir, "server"); const HASH_RE = /(['"\\])([a-z@][a-z0-9@./_-]+?-[0-9a-f]{16}(?:\/[^'"\\]+)?)\1/g; let patchedFiles = 0; let patchedMatches = 0; @@ -392,6 +394,12 @@ export function assembleStandalone({ if (!distDir) throw new Error("[assembleStandalone] distDir is required"); if (!outDir) throw new Error("[assembleStandalone] outDir is required"); + // 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")); const resolvedOutDir = path.resolve(outDir); if (!fsSync.existsSync(standaloneDir)) { @@ -425,9 +433,13 @@ export function assembleStandalone({ } } - // 2. Copy /static -> resolvedOutDir/.next/static + // 2. Copy /static -> resolvedOutDir//static + // CRITICAL: the standalone server.js is built with distDir baked into its config + // (e.g. "./.build/next"), so it serves /_next/static from //static, + // NOT a literal /.next/static. Copying to .next/static leaves the server's + // static dir empty → every JS/CSS chunk 404s → blank page. Mirror the distDir path. const staticSrc = path.join(distDir, "static"); - const staticDest = path.join(resolvedOutDir, ".next", "static"); + const staticDest = path.join(resolvedOutDir, relDistDir, "static"); if (fsSync.existsSync(staticSrc)) { fsSync.mkdirSync(path.dirname(staticDest), { recursive: true }); fsSync.cpSync(staticSrc, staticDest, { recursive: true, force: true }); @@ -442,7 +454,7 @@ export function assembleStandalone({ // 4. Optionally sanitize abs paths if (sanitizePaths) { - const count = assemblePathSanitize(projectRoot, resolvedOutDir); + const count = assemblePathSanitize(projectRoot, resolvedOutDir, relDistDir); if (count > 0) { console.log(`[assembleStandalone] Sanitised ${count} hardcoded path references`); } @@ -450,7 +462,7 @@ export function assembleStandalone({ // 5. Optionally patch Turbopack hashed chunks if (doPatchChunks) { - const { patchedFiles, patchedMatches } = patchTurbopackChunks(resolvedOutDir); + const { patchedFiles, patchedMatches } = patchTurbopackChunks(resolvedOutDir, relDistDir); if (patchedMatches > 0) { console.log( `[assembleStandalone] Hash-strip: patched ${patchedMatches} hashed require() in ${patchedFiles} server chunk file(s)` diff --git a/tests/unit/build/assemble-standalone.test.ts b/tests/unit/build/assemble-standalone.test.ts index 8acb513f33..6aa0f42b51 100644 --- a/tests/unit/build/assemble-standalone.test.ts +++ b/tests/unit/build/assemble-standalone.test.ts @@ -20,7 +20,16 @@ test("assembleStandalone copies standalone + static + public + sidecars into out assembleStandalone({ distDir, outDir, projectRoot: tmp, sanitizePaths: false, copyNatives: false }); assert.ok(fs.existsSync(path.join(outDir, "server.js")), "server.js copied"); - assert.ok(fs.existsSync(path.join(outDir, ".next/static/x.js")), "static copied"); + // Static lands under the distDir path (.build/next/static), where the standalone + // server.js — built with distDir baked into its config — serves /_next/static from. + assert.ok( + fs.existsSync(path.join(outDir, ".build/next/static/x.js")), + "static copied under distDir" + ); + assert.ok( + !fs.existsSync(path.join(outDir, ".next/static/x.js")), + "static is NOT placed under a literal .next (would 404 against distDir server)" + ); assert.ok(fs.existsSync(path.join(outDir, "public/logo.svg")), "public copied"); fs.rmSync(tmp, { recursive: true, force: true }); });