diff --git a/.github/actions/publish-artifacts/package.json b/.github/actions/publish-artifacts/package.json index 39850bb0..b24b166c 100644 --- a/.github/actions/publish-artifacts/package.json +++ b/.github/actions/publish-artifacts/package.json @@ -1,6 +1,6 @@ { "name": "@onelauncher/action-publish-artifacts", - "version": "1.0.0-alpha.6", + "version": "1.0.0", "private": true, "description": "Next-generation open source Minecraft launcher", "license": "GPL-3.0-only", diff --git a/Cargo.lock b/Cargo.lock index 1c67fc9a..2fcee45c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3722,7 +3722,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oneclient_gui" -version = "1.0.0-alpha.6" +version = "1.0.0" dependencies = [ "anyhow", "chrono", @@ -3758,7 +3758,7 @@ dependencies = [ [[package]] name = "onelauncher_core" -version = "1.0.0-alpha.6" +version = "1.0.0" dependencies = [ "anyhow", "async-compression", @@ -3822,7 +3822,7 @@ dependencies = [ [[package]] name = "onelauncher_entity" -version = "1.0.0-alpha.6" +version = "1.0.0" dependencies = [ "chrono", "interpulse", @@ -3838,7 +3838,7 @@ dependencies = [ [[package]] name = "onelauncher_gamemode" -version = "1.0.0-alpha.6" +version = "1.0.0" dependencies = [ "cc", "libc", @@ -3846,7 +3846,7 @@ dependencies = [ [[package]] name = "onelauncher_gui" -version = "1.0.0-alpha.6" +version = "1.0.0" dependencies = [ "anyhow", "chrono", @@ -3880,7 +3880,7 @@ dependencies = [ [[package]] name = "onelauncher_macro" -version = "1.0.0-alpha.6" +version = "1.0.0" dependencies = [ "proc-macro2", "quote", @@ -3889,7 +3889,7 @@ dependencies = [ [[package]] name = "onelauncher_migration" -version = "1.0.0-alpha.6" +version = "1.0.0" dependencies = [ "async-trait", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 68254b24..c7c15252 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ homepage = "https://polyfrost.org/projects/onelauncher" license = "GPL-3.0-only" readme = "README.md" repository = "https://github.com/Polyfrost/OneLauncher" -version = "1.0.0-alpha.6" +version = "1.0.0" [workspace.dependencies] # workspace diff --git a/README.md b/README.md index 3ff8f8cf..0da79e6c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,14 @@ If you encounter any issues, ensure that you are using the following versions (o - `node` version: **22.15** - `pnpm` version: **10.13** +### Versioning + +OneClient and OneLauncher share a single version across workspace manifests. + +- Set an explicit version: `pnpm version:bump 1.0.0-alpha.7` +- Increment using semver: `pnpm version:bump prerelease` +- Preview changes only: `pnpm version:bump prerelease --dry-run` + ### Project Structure - **`packages/`** - Shared libraries and utilities. - [**`core/`**](./packages/core/) - Launcher Core. This is the library that contains the entire launcher logic. diff --git a/apps/oneclient/desktop/package.json b/apps/oneclient/desktop/package.json index ebfada5e..d46f8273 100644 --- a/apps/oneclient/desktop/package.json +++ b/apps/oneclient/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@oneclient/desktop", "type": "module", - "version": "1.0.0-alpha.6", + "version": "1.0.0", "private": true, "description": "Next-generation open source Minecraft launcher", "license": "GPL-3.0-only", diff --git a/apps/onelauncher/desktop/package.json b/apps/onelauncher/desktop/package.json index ef0031a7..cae040f5 100644 --- a/apps/onelauncher/desktop/package.json +++ b/apps/onelauncher/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@onelauncher/desktop", "type": "module", - "version": "1.0.0-alpha.6", + "version": "1.0.0", "private": true, "description": "Next-generation open source Minecraft launcher", "license": "GPL-3.0-only", diff --git a/package.json b/package.json index c7e2b05d..618bb3d1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@onelauncher/root", "type": "module", - "version": "1.0.0-alpha.6", + "version": "1.0.0", "private": true, "engineStrict": true, "description": "Next-generation open source Minecraft launcher and tools for interacting with Polyfrost", @@ -22,6 +22,7 @@ }, "scripts": { "prep": "pnpm scripts prep", + "version:bump": "pnpm scripts version", "typecheck": "pnpm -r typecheck", "bootstrap": "cargo clean && ./packages/scripts/setup.sh && pnpm install && pnpm prep", "core": "pnpm --filter @onelauncher/core -- ", diff --git a/packages/core/package.json b/packages/core/package.json index 6f87b76e..8a91f728 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@onelauncher/core", - "version": "1.0.0-alpha.6", + "version": "1.0.0", "description": "Next-generation open source Minecraft launcher core", "license": "GPL-3.0-only", "repository": { diff --git a/packages/gamemode/package.json b/packages/gamemode/package.json index 391d94d3..4354218b 100644 --- a/packages/gamemode/package.json +++ b/packages/gamemode/package.json @@ -1,6 +1,6 @@ { "name": "@onelauncher/gamemode", - "version": "1.0.0-alpha.6", + "version": "1.0.0", "description": "Next-generation open source Minecraft launcher feral gamemode bindings", "license": "GPL-3.0-only", "repository": { diff --git a/packages/scripts/package.json b/packages/scripts/package.json index dcaebdf4..014a3520 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,7 +1,7 @@ { "name": "@onelauncher/scripts", "type": "module", - "version": "1.0.0-alpha.6", + "version": "1.0.0", "private": true, "description": "Scripts for developing OneLauncher", "license": "GPL-3.0-only", @@ -16,6 +16,7 @@ "types": "./src/index.ts", "scripts": { "tauri": "pnpm tsx tauri.ts", + "version": "pnpm tsx version.ts", "prep": "pnpm tsx prepare.ts", "setup-win": "./setup.ps1", "setup-nix": "./setup.sh", diff --git a/packages/scripts/version.ts b/packages/scripts/version.ts new file mode 100644 index 00000000..cb954797 --- /dev/null +++ b/packages/scripts/version.ts @@ -0,0 +1,235 @@ +import type { ReleaseType } from 'semver'; +import { readFile, writeFile } from 'node:fs/promises'; +import process from 'node:process'; +import { consola } from 'consola'; +import { join } from 'pathe'; +import semver from 'semver'; +import { parse as parseTOML } from 'smol-toml'; +import { checkEnvironment } from './utils'; + +const env = checkEnvironment(import.meta); + +const RELEASE_TYPES: Set = new Set([ + 'major', + 'minor', + 'patch', + 'premajor', + 'preminor', + 'prepatch', + 'prerelease', +]); + +const VERSIONED_PACKAGE_JSON_PATHS = [ + 'package.json', + 'packages/core/package.json', + 'packages/gamemode/package.json', + 'packages/scripts/package.json', + 'apps/oneclient/desktop/package.json', + 'apps/onelauncher/desktop/package.json', + '.github/actions/publish-artifacts/package.json', +] as const; + +interface PackageJsonLike { + version?: unknown; + [key: string]: unknown; +} + +interface CargoWorkspaceManifest { + workspace?: { + members?: unknown; + }; + package?: { + name?: unknown; + version?: unknown; + }; +} + +function usage() { + consola.info(` +usage: + pnpm version:bump + pnpm version:bump [preid] + pnpm version:bump <...> --dry-run +`); +} + +function inferPreid(version: string): string { + const prerelease = semver.prerelease(version); + const preid = prerelease?.find(part => typeof part === 'string'); + return typeof preid === 'string' ? preid : 'alpha'; +} + +function hasWorkspaceVersion(value: unknown): boolean { + return typeof value === 'object' + && value !== null + && 'workspace' in value + && (value as { workspace?: unknown }).workspace === true; +} + +function resolveNextVersion(currentVersion: string, positionalArgs: Array): string | null { + const [firstArg, secondArg] = positionalArgs; + if (!firstArg) + return null; + + if (RELEASE_TYPES.has(firstArg as ReleaseType)) { + const releaseType = firstArg as ReleaseType; + const usePreid = releaseType.startsWith('pre') || releaseType === 'prerelease'; + if (!usePreid && secondArg) + return null; + return semver.inc(currentVersion, releaseType, usePreid ? (secondArg ?? inferPreid(currentVersion)) : undefined); + } + + if (secondArg) + return null; + + return semver.valid(firstArg); +} + +async function readRootVersion(): Promise { + const raw = await readFile(join(env.__root, 'package.json'), 'utf8'); + const parsed = JSON.parse(raw) as PackageJsonLike; + if (typeof parsed.version !== 'string') + throw new Error('root package.json does not contain a valid version string'); + + return parsed.version; +} + +async function updatePackageJson(path: string, version: string, dryRun: boolean): Promise { + const fullPath = join(env.__root, path); + const raw = await readFile(fullPath, 'utf8'); + const parsed = JSON.parse(raw) as PackageJsonLike; + + if (typeof parsed.version !== 'string') + throw new Error(`${path} does not contain a valid version string`); + + if (parsed.version === version) + return false; + + parsed.version = version; + if (!dryRun) + await writeFile(fullPath, `${JSON.stringify(parsed, null, '\t')}\n`, 'utf8'); + + return true; +} + +async function getWorkspacePackageNames(): Promise> { + const workspaceManifestPath = join(env.__root, 'Cargo.toml'); + const workspaceRaw = await readFile(workspaceManifestPath, 'utf8'); + const workspaceManifest = parseTOML(workspaceRaw) as CargoWorkspaceManifest; + const members = workspaceManifest.workspace?.members; + + if (!Array.isArray(members)) + throw new Error('failed to parse workspace members from Cargo.toml'); + + const names: Set = new Set(); + + await Promise.all(members.map(async (member) => { + if (typeof member !== 'string') + return; + + const memberManifestPath = join(env.__root, member, 'Cargo.toml'); + const memberRaw = await readFile(memberManifestPath, 'utf8'); + const memberManifest = parseTOML(memberRaw) as CargoWorkspaceManifest; + + const crateName = memberManifest.package?.name; + const crateVersion = memberManifest.package?.version; + if (typeof crateName === 'string' && hasWorkspaceVersion(crateVersion)) + names.add(crateName); + })); + + return names; +} + +async function updateCargoWorkspaceVersion(version: string, dryRun: boolean): Promise { + const cargoPath = join(env.__root, 'Cargo.toml'); + const cargoRaw = await readFile(cargoPath, 'utf8'); + const workspaceVersionPattern = /(\[workspace\.package\][\s\S]*?\nversion\s*=\s*")([^"]+)(")/; + if (!workspaceVersionPattern.test(cargoRaw)) + throw new Error('failed to find [workspace.package].version in Cargo.toml'); + + const nextCargo = cargoRaw.replace(workspaceVersionPattern, `$1${version}$3`); + if (nextCargo === cargoRaw) + return false; + + if (!dryRun) + await writeFile(cargoPath, nextCargo, 'utf8'); + + return true; +} + +async function updateCargoLock(version: string, crateNames: Set, dryRun: boolean): Promise { + const cargoLockPath = join(env.__root, 'Cargo.lock'); + const cargoLockRaw = await readFile(cargoLockPath, 'utf8'); + const packagePattern = /(\[\[package\]\]\r?\nname = "([^"]+)"\r?\nversion = ")([^"]+)(")/g; + + const nextCargoLock = cargoLockRaw.replace(packagePattern, (match, prefix, packageName, currentVersion, suffix) => { + if (!crateNames.has(packageName)) + return match; + + if (currentVersion === version) + return match; + + return `${prefix}${version}${suffix}`; + }); + + if (nextCargoLock === cargoLockRaw) + return false; + + if (!dryRun) + await writeFile(cargoLockPath, nextCargoLock, 'utf8'); + + return true; +} + +async function main() { + const rawArgs = process.argv.slice(2); + const dryRun = rawArgs.includes('--dry-run'); + const positionalArgs = rawArgs.filter(arg => arg !== '--dry-run'); + + if (positionalArgs.length === 0 || positionalArgs.length > 2) { + usage(); + throw new Error('invalid arguments'); + } + + const currentVersion = await readRootVersion(); + const nextVersion = resolveNextVersion(currentVersion, positionalArgs); + if (!nextVersion) { + usage(); + throw new Error('unable to resolve target version'); + } + + if (currentVersion === nextVersion) { + consola.warn(`version already set to ${nextVersion}`); + return; + } + + consola.start(`${dryRun ? 'previewing' : 'bumping'} version: ${currentVersion} -> ${nextVersion}`); + const changedPaths: Array = []; + + for (const packagePath of VERSIONED_PACKAGE_JSON_PATHS) + if (await updatePackageJson(packagePath, nextVersion, dryRun)) + changedPaths.push(packagePath); + + if (await updateCargoWorkspaceVersion(nextVersion, dryRun)) + changedPaths.push('Cargo.toml'); + + const crateNames = await getWorkspacePackageNames(); + if (await updateCargoLock(nextVersion, crateNames, dryRun)) + changedPaths.push('Cargo.lock'); + + if (changedPaths.length === 0) { + consola.warn('no files required changes'); + return; + } + + const action = dryRun ? 'would update' : 'updated'; + consola.success(`${action} ${changedPaths.length} files:`); + changedPaths.forEach((path) => { + consola.info(`- ${path}`); + }); +} + +void main().catch((error: unknown) => { + consola.error(error instanceof Error ? error.message : String(error)); + env.__exit(1); +});