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 <diegosouzapw@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-05 16:07:31 -03:00
committed by GitHub
parent 5e344a3a99
commit 9e3126828e
3 changed files with 26 additions and 4 deletions

View File

@@ -0,0 +1 @@
- fix(auto-update): skip synthetic Next.js standalone package.json without `name` field in resolveProjectRoot (#8956)

View File

@@ -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;

View File

@@ -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.