import { readFile, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const frontendDir = fileURLToPath(new URL("../", import.meta.url)); const paths = { packageJson: new URL("../package.json", import.meta.url), packageLock: new URL("../package-lock.json", import.meta.url), tauriConfig: new URL("../src-tauri/tauri.conf.json", import.meta.url), cargoToml: new URL("../src-tauri/Cargo.toml", import.meta.url), cargoLock: new URL("../src-tauri/Cargo.lock", import.meta.url), }; const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; const readJson = async (path) => JSON.parse(await readFile(path, "utf8")); const writeJson = async (path, value) => writeFile(path, `${JSON.stringify(value, null, 2)}\n`); const readCargoPackageVersion = async () => { const cargo = await readFile(paths.cargoToml, "utf8"); const packageSection = cargo.match(/\[package\]([\s\S]*?)(?=\n\[|$)/)?.[1]; const version = packageSection?.match(/^version\s*=\s*"([^"]+)"/m)?.[1]; if (!version) throw new Error("Cannot find [package].version in src-tauri/Cargo.toml"); return version; }; const readCargoLockPackageVersion = async () => { const cargoLock = await readFile(paths.cargoLock, "utf8"); const packageSection = cargoLock .match(/\[\[package\]\]([\s\S]*?)(?=\n\[\[package\]\]|$)/g) ?.find((section) => /^name\s*=\s*"ctms-desktop"$/m.test(section)); const version = packageSection?.match(/^version\s*=\s*"([^"]+)"/m)?.[1]; if (!version) throw new Error("Cannot find ctms-desktop version in src-tauri/Cargo.lock"); return version; }; const readVersions = async () => { const [packageJson, packageLock, tauriConfig, cargoVersion, cargoLockVersion] = await Promise.all([ readJson(paths.packageJson), readJson(paths.packageLock), readJson(paths.tauriConfig), readCargoPackageVersion(), readCargoLockPackageVersion(), ]); return { "package.json": packageJson.version, "package-lock.json": packageLock.version, "package-lock.json root": packageLock.packages?.[""]?.version, "tauri.conf.json": tauriConfig.version, "Cargo.toml": cargoVersion, "Cargo.lock": cargoLockVersion, }; }; const assertVersionsMatch = async () => { const versions = await readVersions(); const uniqueVersions = new Set(Object.values(versions)); if (uniqueVersions.size !== 1 || uniqueVersions.has(undefined)) { const details = Object.entries(versions) .map(([file, version]) => ` ${file}: ${version ?? ""}`) .join("\n"); throw new Error(`Client versions are not synchronized:\n${details}`); } const [version] = uniqueVersions; console.log(`Client version ${version} is synchronized.`); }; const setVersion = async (version) => { if (!SEMVER_PATTERN.test(version)) { throw new Error(`Invalid semantic version: ${version}`); } const [packageJson, packageLock, tauriConfig, tauriConfigSource, cargo, cargoLock] = await Promise.all([ readJson(paths.packageJson), readJson(paths.packageLock), readJson(paths.tauriConfig), readFile(paths.tauriConfig, "utf8"), readFile(paths.cargoToml, "utf8"), readFile(paths.cargoLock, "utf8"), ]); packageJson.version = version; packageLock.version = version; packageLock.packages[""].version = version; const tauriVersionPattern = /("version"\s*:\s*")[^"]+(")/; const packageSectionPattern = /(\[package\][\s\S]*?^version\s*=\s*")[^"]+(")/m; const lockPackagePattern = /(\[\[package\]\]\nname\s*=\s*"ctms-desktop"\nversion\s*=\s*")[^"]+(")/m; if (typeof tauriConfig.version !== "string" || !tauriVersionPattern.test(tauriConfigSource)) { throw new Error("Cannot update version in src-tauri/tauri.conf.json"); } if (!packageSectionPattern.test(cargo)) { throw new Error("Cannot update [package].version in src-tauri/Cargo.toml"); } if (!lockPackagePattern.test(cargoLock)) { throw new Error("Cannot update ctms-desktop version in src-tauri/Cargo.lock"); } await Promise.all([ writeJson(paths.packageJson, packageJson), writeJson(paths.packageLock, packageLock), writeFile(paths.tauriConfig, tauriConfigSource.replace(tauriVersionPattern, `$1${version}$2`)), writeFile(paths.cargoToml, cargo.replace(packageSectionPattern, `$1${version}$2`)), writeFile(paths.cargoLock, cargoLock.replace(lockPackagePattern, `$1${version}$2`)), ]); console.log(`Updated CTMS Web and Desktop client version to ${version} in ${frontendDir}`); await assertVersionsMatch(); }; const [, , command = "--check", value] = process.argv; try { if (command === "--check") { await assertVersionsMatch(); } else if (command === "--set" && value) { await setVersion(value); } else { throw new Error("Usage: node scripts/client-version.mjs [--check | --set ]"); } } catch (error) { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; }