'use strict'; // 每個版本號區段的進位上限:patch / minor 達到 10 即向上進位 const SEGMENT_LIMIT = 10; /** * 逐區段(element-wise)比較兩個版本號數值陣列,較短的陣列視為較小。 * @param {number[]} a - 第一個版本號區段陣列,例如 [1, 2, 3]。 * @param {number[]} b - 第二個版本號區段陣列,例如 [1, 2, 0]。 * @returns {number} 大於 0 表示 a 大於 b;小於 0 表示 a 小於 b;0 表示兩者相等。 * 相異區段回傳的是該段差值(非固定 ±1)。 * @throws {TypeError} 當 a 或 b 為 null/undefined(讀取 .length 即報錯)時拋出; * 傳入「有 length 但非數值陣列」(如字串)不會拋例外,但會得到非預期結果。 */ function compareVersionArrays(a, b) { const length = Math.max(a.length, b.length); for (let i = 0; i < length; i += 1) { if (a[i] === undefined) { return -1; } if (b[i] === undefined) { return 1; } if (a[i] !== b[i]) { return a[i] - b[i]; } } return 0; } /** * 從 release 清單解析出所有穩定版(排除 beta、排除格式不合法者)的版本號數值陣列。 * 會去除 tag 開頭的 'v',並將版本字串切成數字陣列。 * @param {Array<{tag_name?: string}>} releases - release 物件清單;每個項目以 tag_name 表示版本標籤。 * @returns {number[][]} 穩定版版本號的數值陣列清單,例如 [[1,2,3],[1,2,0]];輸入非陣列時回傳空陣列。 */ function parseStableVersions(releases) { if (!Array.isArray(releases)) { return []; } return releases .filter((release) => release && typeof release.tag_name === 'string' && !release.tag_name.includes('-beta.')) .map((release) => release.tag_name.replace(/^v/, '')) .filter((tag) => /^[0-9]+(\.[0-9]+)*$/.test(tag)) .map((tag) => tag.split('.').map(Number)); } /** * 取得 release 清單中最新(最大)的穩定版版本號字串,固定格式化為三段 "major.minor.patch"。 * @param {Array<{tag_name?: string}>} releases - release 物件清單。 * @returns {string} 最新穩定版版本號字串;查無穩定版時回傳 "0.0.0"。 */ function latestStableVersion(releases) { const versions = parseStableVersions(releases); if (versions.length === 0) { return '0.0.0'; } const max = versions.reduce( (best, current) => (compareVersionArrays(current, best) > 0 ? current : best), ); return `${max[0] ?? 0}.${max[1] ?? 0}.${max[2] ?? 0}`; } /** * 依最新穩定版字串計算下一個發行版本號:patch 加 1,patch 達 10 進位至 minor,minor 達 10 進位至 major。 * @param {string} latest - 最新穩定版版本號字串,例如 "1.2.9"。 * @returns {string} 下一個發行版本號字串,格式為 "major.minor.patch",例如 "1.3.0"。 * @remarks 「逢 10 進位」為本專案自訂約定(非標準 SemVer);輸入會先 String() 轉字串, * 缺段或非數字段一律補 0(如 null/undefined → "0.0.1"),major 無進位上限。 */ function nextReleaseVersion(latest) { const parts = String(latest).split('.').map((part) => Number(part) || 0); let [major = 0, minor = 0, patch = 0] = parts; patch += 1; if (patch >= SEGMENT_LIMIT) { patch = 0; minor += 1; } if (minor >= SEGMENT_LIMIT) { minor = 0; major += 1; } return `${major}.${minor}.${patch}`; } /** * 計算指定版本號的下一個 beta 流水號:取現有相符 beta 標籤的最大序號加 1,查無時回傳 1。 * @param {Array<{tag_name?: string}>} releases - release 物件清單。 * @param {string} version - 不含前綴 'v' 的版本號字串,例如 "1.3.0";用於組出 "v-beta." 前綴比對。 * @returns {number} 下一個 beta 流水號;查無對應 beta 時回傳 1。 */ function nextBetaNumber(releases, version) { if (!Array.isArray(releases)) { return 1; } const prefix = `v${version}-beta.`; const numbers = releases .filter((release) => release && typeof release.tag_name === 'string' && release.tag_name.startsWith(prefix)) .map((release) => Number(release.tag_name.slice(prefix.length))) .filter((number) => Number.isFinite(number)); const max = numbers.length > 0 ? Math.max(...numbers) : 0; return max + 1; } /** * 依「已知的最新穩定版」計算本次要使用的版本號;isBeta 為 true 時產生 beta 版本號。 * * 此函式不負責「取得舊版本」(latest 由呼叫端傳入),僅負責「計算版本號」這一步, * 讓取得舊版本與計算新版本兩個階段可各自獨立。 * * @param {string} latest - 已知的最新穩定版版本號字串(如 "1.2.3";查無時為 "0.0.0")。 * @param {Array<{tag_name?: string}>} releases - release 物件清單;beta 流水號需據此比對。 * @param {boolean} isBeta - 是否產生 beta 版本號;true 時回傳形如 "-beta."。 * @returns {string} 本次要使用的版本號字串;非 beta 為下一個發行版(如 "1.3.0"), * beta 為 "-beta."(如 "1.3.0-beta.1")。 */ function nextVersion(latest, releases, isBeta) { const next = nextReleaseVersion(latest); return isBeta ? `${next}-beta.${nextBetaNumber(releases, next)}` : next; } /** * 計算最新穩定版與下一個版本號;isBeta 為 true 時產生 beta 版本號。 * @param {Array<{tag_name?: string}>} releases - release 物件清單。 * @param {boolean} isBeta - 是否產生 beta 版本號;true 時 version 形如 "-beta."。 * @returns {{latest: string, version: string}} 回傳物件: * - latest: 現有最新穩定版版本號字串(查無時為 "0.0.0")。 * - version: 本次要使用的版本號字串;非 beta 為下一個發行版(如 "1.3.0"),beta 為 "-beta."(如 "1.3.0-beta.1")。 */ function calculateVersion(releases, isBeta) { const latest = latestStableVersion(releases); return { latest, version: nextVersion(latest, releases, isBeta) }; } module.exports = { compareVersionArrays, parseStableVersions, latestStableVersion, nextReleaseVersion, nextBetaNumber, nextVersion, calculateVersion, };