123456const GRADLE_FILES = [7 "app/build.gradle.kts",8 "app/build.gradle",9 "build.gradle.kts",10 "build.gradle",11];12const VERSION = /^[A-Za-z0-9.-]{1,32}$/;131415export function declaredVersion(read: (path: string) => string | null): string | null {16 const sources: (() => string | null)[] = [17 () => read("VERSION")?.trim() || null,18 () => lineVersion(read("build.zig.zon"), ".version"),19 () => lineVersion(read("v.mod"), "version"),20 () => lineVersion(read("package.json"), '"version"'),21 () => lineVersion(read("Cargo.toml"), "version"),22 () => plistVersion(read("Info.plist")),23 ...GRADLE_FILES.map((path) => () => gradleVersion(read(path))),24 ];25 for (const source of sources) {26 const value = source();27 if (value !== null) return VERSION.test(value) ? value : null;28 }29 return null;30}313233function lineVersion(text: string | null, key: string): string | null {34 for (const line of text?.split(/\r?\n/) ?? []) {35 const trimmed = line.replace(/^[ \t]+/, "");36 if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("#")) continue;37 if (!trimmed.startsWith(key)) continue;38 const value = literalAfter(trimmed, key.length);39 if (value !== null) return value;40 }41 return null;42}434445function literalAfter(line: string, from: number): string | null {46 let at = from;47 while (at < line.length && " \t=:".includes(line[at] as string)) at++;48 const quote = line[at];49 if (quote !== '"' && quote !== "'") return null;50 const end = line.indexOf(quote, at + 1);51 return end < 0 ? null : line.slice(at + 1, end);52}535455function plistVersion(text: string | null): string | null {56 const at = text?.indexOf("<key>CFBundleShortVersionString</key>") ?? -1;57 if (!text || at < 0) return null;58 const rest = text.slice(at);59 const open = rest.indexOf("<string>");60 const close = rest.indexOf("</string>");61 if (open < 0 || close <= open + "<string>".length) return null;62 return rest.slice(open + "<string>".length, close).trim() || null;63}64656667function gradleVersion(text: string | null): string | null {68 for (const line of text?.split(/\r?\n/) ?? []) {69 const trimmed = line.replace(/^[ \t]+/, "");70 if (trimmed === "" || /^(\/\/|\/\*|\*)/.test(trimmed)) continue;71 if (!/^versionName[ =]/.test(trimmed)) continue;72 const value = literalAfter(trimmed, "versionName".length);73 if (value !== null) return value;74 }75 return null;76}