From 1168722a284228f90e8783e430b7b909dd00756c Mon Sep 17 00:00:00 2001 From: PRGM Date: Fri, 10 Jul 2026 02:35:10 -0400 Subject: [PATCH] Harden version file updates in set-version script - Parse JSON before updating version fields - Ensure only the top-level version key is replaced - Improve error handling for invalid or ambiguous files --- scripts/set-version.mjs | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/scripts/set-version.mjs b/scripts/set-version.mjs index 55da3f4..0537a8b 100644 --- a/scripts/set-version.mjs +++ b/scripts/set-version.mjs @@ -9,14 +9,42 @@ if (!version || !semverPattern.test(version)) { async function updateJson(path) { const source = await readFile(path, "utf8"); - const pattern = /("version"\s*:\s*")[^"]+("\s*,)/; - const matches = source.match(new RegExp(pattern.source, "g")); + let document; - if (matches?.length !== 1) { - throw new Error(`Expected exactly one top-level version in ${path}`); + try { + document = JSON.parse(source); + } catch (error) { + throw new Error(`Expected valid JSON in ${path}`, { cause: error }); } - await writeFile(path, source.replace(pattern, `$1${version}$2`)); + if ( + !document || + typeof document !== "object" || + Array.isArray(document) || + typeof document.version !== "string" + ) { + throw new Error(`Expected one top-level string version in ${path}`); + } + + const versionProperties = [ + ...source.matchAll(/^([ \t]*)"version"(\s*:\s*)"([^"\r\n]*)"/gm), + ]; + const minimumIndent = Math.min(...versionProperties.map((match) => match[1].length)); + const topLevelMatches = versionProperties.filter( + (match) => match[1].length === minimumIndent, + ); + + if (topLevelMatches.length !== 1 || topLevelMatches[0][3] !== document.version) { + throw new Error(`Could not locate the top-level version in ${path}`); + } + + const match = topLevelMatches[0]; + const start = match.index; + const replacement = `${match[1]}"version"${match[2]}"${version}"`; + await writeFile( + path, + `${source.slice(0, start)}${replacement}${source.slice(start + match[0].length)}`, + ); } async function replaceVersion(path, pattern, label) {