From 9e3126828e8a3bac113e8a375e592ef940ab551b Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 5 Aug 2026 16:07:31 -0300 Subject: [PATCH] fix(auto-update): skip synthetic Next.js standalone package.json without name field in resolveProjectRoot (#8956) (#9354) A Next.js standalone build writes a synthetic .build/next/package.json ({"type":"commonjs"}) that lacks a "name" field. The resolveProjectRoot() walk-up was stopping at this marker instead of continuing to the real repo root, making PROJECT_ROOT point at .build/next where no .git exists, which caused the source-mode validation to report "Not a git repository." Fix: only accept a package.json as a project-root marker when its parsed content has a non-empty "name" field. Keep .git as a hard marker. Add isValidPackageMarker() helper for testability. Co-authored-by: diegosouzapw --- changelog.d/fixes/8956-fix.plan.md | 1 + src/lib/system/autoUpdate.ts | 27 ++++++++++++++++++++++++--- tests/unit/auto-update.test.ts | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fixes/8956-fix.plan.md diff --git a/changelog.d/fixes/8956-fix.plan.md b/changelog.d/fixes/8956-fix.plan.md new file mode 100644 index 0000000000..a5e4892c00 --- /dev/null +++ b/changelog.d/fixes/8956-fix.plan.md @@ -0,0 +1 @@ +- fix(auto-update): skip synthetic Next.js standalone package.json without `name` field in resolveProjectRoot (#8956) \ No newline at end of file diff --git a/src/lib/system/autoUpdate.ts b/src/lib/system/autoUpdate.ts index cd10b6edcb..582a04a0b6 100644 --- a/src/lib/system/autoUpdate.ts +++ b/src/lib/system/autoUpdate.ts @@ -1,5 +1,5 @@ import { execFile, spawn } from "node:child_process"; -import { closeSync, mkdirSync, openSync, existsSync } from "node:fs"; +import { closeSync, mkdirSync, openSync, existsSync, readFileSync } from "node:fs"; import { access } from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; @@ -7,15 +7,36 @@ import { homedir } from "node:os"; const execFileAsync = promisify(execFile); +/** + * Check whether a directory's package.json is a valid project-root marker by + * requiring a non-empty `name` field. The Next.js standalone build writes a + * synthetic `.build/next/package.json` = `{"type":"commonjs"}` that should not + * be mistaken for the real project root. + * + * Swallows read / parse errors (missing file, invalid JSON) and returns false + * so the walk-up continues. + * + * @internal — exported for testability. + */ +export function isValidPackageMarker(dir: string): boolean { + try { + const content = readFileSync(path.join(dir, "package.json"), "utf-8"); + const pkg = JSON.parse(content); + return typeof pkg.name === "string" && pkg.name.length > 0; + } catch { + return false; + } +} + /** @internal — exported for testability. */ export function resolveProjectRoot( fallback: string, startDir: string = typeof __dirname !== "undefined" ? __dirname : process.cwd() ): string { - const markers = ["package.json", ".git"] as const; let dir = path.resolve(startDir); while (true) { - if (markers.some((m) => existsSync(path.join(dir, m)))) return dir; + if (existsSync(path.join(dir, ".git"))) return dir; + if (existsSync(path.join(dir, "package.json")) && isValidPackageMarker(dir)) return dir; const parent = path.dirname(dir); if (parent === dir) break; dir = parent; diff --git a/tests/unit/auto-update.test.ts b/tests/unit/auto-update.test.ts index a6714cb765..55112bae62 100644 --- a/tests/unit/auto-update.test.ts +++ b/tests/unit/auto-update.test.ts @@ -404,7 +404,7 @@ test("resolveProjectRoot walks up from start dir to nearest package.json or .git const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-root-")); const subDir = path.join(tempRoot, "sub", "deep"); fs.mkdirSync(subDir, { recursive: true }); - fs.writeFileSync(path.join(tempRoot, "package.json"), "{}"); + fs.writeFileSync(path.join(tempRoot, "package.json"), JSON.stringify({ name: "omniroute" })); try { // Walking up from a deep subdir that does not have markers must find the real root.