fix(startup): best-effort self-heal for corrupted Turbopack dev cache on Windows (#6289) (#6445)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-06 18:15:58 -03:00
committed by GitHub
parent c50a83a94d
commit f1a02f602b
4 changed files with 194 additions and 9 deletions

View File

@@ -11,6 +11,10 @@ import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
import { ensurePeerStampToken, stampPeerIp } from "./peer-stamp.mjs";
import methodGuard from "./http-method-guard.cjs";
import { ensureNativeSqlite } from "./ensure-native-sqlite.mjs";
import {
isTurbopackCacheCorruption,
purgeAllTurbopackCaches,
} from "./turbopackCacheHeal.mjs";
import { randomUUID } from "node:crypto";
const { maybeHandleDisallowedMethod } = methodGuard;
@@ -88,17 +92,46 @@ ensurePeerStampToken();
if (!useTurbopack) {
delete process.env.TURBOPACK;
}
const nextApp = next({
dev,
dir: process.cwd(),
hostname,
port: dashboardPort,
turbopack: useTurbopack,
webpack: !useTurbopack,
});
function createNextApp() {
return next({
dev,
dir: process.cwd(),
hostname,
port: dashboardPort,
turbopack: useTurbopack,
webpack: !useTurbopack,
});
}
let nextApp = createNextApp();
// Best-effort self-heal for a corrupted Turbopack persistent dev cache (#6289):
// on Windows an mmap of an SST cache file can fail ("os error 1455" / paging
// file too small), which Turbopack surfaces as a misleading module-resolve
// error. This is an UPSTREAM Turbopack cache-corruption bug — not our code.
// When `prepare()` rejects with that signature we purge the cache and retry
// ONCE. Caveat: the failure often surfaces as a runtime overlay rather than a
// `prepare()` rejection, so this cannot always intercept it — the reliable
// remedy remains manually deleting `.build/next/**/cache/turbopack`.
async function prepareWithHeal() {
try {
await nextApp.prepare();
} catch (error) {
const detail = error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error);
if (!useTurbopack || !isTurbopackCacheCorruption(detail)) throw error;
console.warn(
"[Next] Turbopack dev cache looks corrupted (Windows mmap / os error 1455 — known upstream bug). Purging and retrying once…"
);
const removed = purgeAllTurbopackCaches();
for (const dir of removed) console.warn(`[Next] purged Turbopack cache: ${dir}`);
nextApp = createNextApp();
await nextApp.prepare();
console.warn("[Next] Turbopack dev cache purged; startup retry succeeded.");
}
}
async function start() {
await nextApp.prepare();
await prepareWithHeal();
const requestHandler = nextApp.getRequestHandler();
const upgradeHandler = nextApp.getUpgradeHandler();

View File

@@ -0,0 +1,74 @@
// Best-effort self-heal for a corrupted Turbopack persistent dev cache.
//
// Context (#6289): on Windows, `pnpm dev` can fail at startup when Turbopack
// mmaps a persistent-cache SST file and the OS refuses the mapping
// ("os error 1455" — "paging file too small"). Turbopack then surfaces a
// misleading `Module not found: Can't resolve '@/shared/utils/machine'`.
// This is a known UPSTREAM Turbopack cache-corruption bug — NOT our code.
// The reliable remedy is deleting the Turbopack cache dir; this module lets
// the dev launcher attempt that automatically once before giving up.
//
// Pure + side-effect-isolated so it can be unit-tested without booting Next.
import fs from "node:fs";
import path from "node:path";
// Signature of the corrupted-cache failure. Kept intentionally broad because
// the same corruption surfaces through several messages (the raw mmap/SST
// error, the Windows paging-file error code, and the misleading module-resolve
// error emitted by Turbopack's "restore task data" step).
const CORRUPTION_SIGNATURE = /restore task data|mmap .*SST|os error 1455|paging file/i;
/**
* True when an error message looks like a corrupted Turbopack persistent cache.
* @param {unknown} message
* @returns {boolean}
*/
export function isTurbopackCacheCorruption(message) {
if (message == null) return false;
return CORRUPTION_SIGNATURE.test(String(message));
}
/**
* Candidate Turbopack cache directories for a given Next dist dir. Next has
* placed the persistent cache under both `<distDir>/cache/turbopack` and
* `<distDir>/dev/cache/turbopack` across versions, so purge both.
* @param {string} [distDir]
* @param {string} [cwd]
* @returns {string[]}
*/
export function turbopackCacheDirs(
distDir = process.env.NEXT_DIST_DIR || ".build/next",
cwd = process.cwd()
) {
const base = path.isAbsolute(distDir) ? distDir : path.join(cwd, distDir);
return [
path.join(base, "cache", "turbopack"),
path.join(base, "dev", "cache", "turbopack"),
];
}
/**
* Recursively remove a single Turbopack cache directory.
* @param {string} dir
* @returns {boolean} true if the directory existed and was removed
*/
export function purgeTurbopackCache(dir) {
if (!dir || !fs.existsSync(dir)) return false;
fs.rmSync(dir, { recursive: true, force: true });
return true;
}
/**
* Purge every candidate Turbopack cache directory for the given dist dir.
* @param {string} [distDir]
* @param {string} [cwd]
* @returns {string[]} the directories that existed and were removed
*/
export function purgeAllTurbopackCaches(distDir, cwd) {
const removed = [];
for (const dir of turbopackCacheDirs(distDir, cwd)) {
if (purgeTurbopackCache(dir)) removed.push(dir);
}
return removed;
}