From 43f412272cdcca8126d5bbd4ed0bcb2415e90e34 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 14 Nov 2025 10:13:24 +0100 Subject: [PATCH] feat: add script to upgrade workspace dependencies Add a new script for upgrading dependencies across workspace packages. The script allows updating a specified dependency to either the latest version or a specific target version across all packages that use it. Features: - Find all packages using a specific dependency - Update to latest or specified version - Support for all dependency types (regular, dev, peer, optional) - Dry run mode to preview changes Issue: BTC-2732 Co-authored-by: llm-git --- package.json | 1 + scripts/upgrade-workspace-dependency.ts | 175 ++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 scripts/upgrade-workspace-dependency.ts diff --git a/package.json b/package.json index 3345696574..0734fd1d77 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "check-commits": "yarn commitlint --from=origin/${GITHUB_REPO_BRANCH:-master} -V", "check-deps": "tsx ./scripts/check-package-dependencies.ts", "check-versions": "node ./check-package-versions.js", + "upgrade-dep": "tsx ./scripts/upgrade-workspace-dependency.ts upgrade", "dev": "tsc -b ./tsconfig.packages.json -w", "prepare": "husky install", "sdk-coin:new": "yo ./scripts/sdk-coin-generator", diff --git a/scripts/upgrade-workspace-dependency.ts b/scripts/upgrade-workspace-dependency.ts new file mode 100644 index 0000000000..3e9bcdad82 --- /dev/null +++ b/scripts/upgrade-workspace-dependency.ts @@ -0,0 +1,175 @@ +/** + * Upgrade Workspace Dependency Script + * + * This script automates upgrading a dependency across all workspace packages in the monorepo. + * It discovers all packages that use the specified dependency via lerna, updates their package.json + * files, and runs a single yarn install to minimize yarn.lock changes. + * + * Usage: + * yarn upgrade-dep -p @bitgo/wasm-utxo -v 1.3.0 + * yarn upgrade-dep -p @bitgo/wasm-utxo # Upgrades to latest version + * yarn upgrade-dep -p @bitgo/wasm-utxo -v 1.3.0 -d # Dry run mode + */ +import execa from 'execa'; +import fs from 'fs/promises'; +import path from 'path'; +import yargs from 'yargs'; +import { getLernaModules } from './prepareRelease/getLernaModules'; + +interface PackageJson { + name: string; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; +} + +interface PackageWithDep { + packagePath: string; + packageName: string; + currentVersion: string; + depType: 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies'; +} + +async function findPackagesWithDependency(depName: string): Promise { + const modules = await getLernaModules(); + const packagesWithDep: PackageWithDep[] = []; + + for (const module of modules) { + const packageJsonPath = path.join(module.location, 'package.json'); + try { + const content = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson: PackageJson = JSON.parse(content); + + for (const depType of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const) { + const deps = packageJson[depType]; + if (deps && deps[depName]) { + packagesWithDep.push({ + packagePath: packageJsonPath, + packageName: packageJson.name, + currentVersion: deps[depName], + depType, + }); + break; // Only record once per package + } + } + } catch (e) { + // Skip if package.json doesn't exist or can't be read + continue; + } + } + + return packagesWithDep; +} + +async function getLatestVersion(packageName: string): Promise { + console.log(`Fetching latest version for ${packageName}...`); + const { stdout } = await execa('npm', ['view', packageName, 'version']); + return stdout.trim(); +} + +async function updatePackageJson( + packagePath: string, + depName: string, + newVersion: string, + depType: string +): Promise { + const content = await fs.readFile(packagePath, 'utf-8'); + const packageJson: PackageJson = JSON.parse(content); + + if (packageJson[depType as keyof PackageJson]) { + const deps = packageJson[depType as keyof PackageJson] as Record; + deps[depName] = newVersion; + } + + await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2) + '\n'); +} + +async function runYarnInstall(): Promise { + console.log('\nRunning yarn install to update lock file...'); + await execa('yarn', ['install'], { + stdio: 'inherit', + }); +} + +async function cmdUpgrade(opts: { package: string; version?: string; dryRun: boolean }): Promise { + const { package: depName, version: targetVersion, dryRun } = opts; + + console.log(`\nšŸ” Searching for packages with dependency: ${depName}\n`); + + const packagesWithDep = await findPackagesWithDependency(depName); + + if (packagesWithDep.length === 0) { + console.log(`āŒ No packages found with dependency: ${depName}`); + return; + } + + console.log(`Found ${packagesWithDep.length} package(s) with ${depName}:\n`); + for (const pkg of packagesWithDep) { + console.log(` • ${pkg.packageName} (${pkg.currentVersion})`); + } + + let newVersion: string; + if (targetVersion) { + newVersion = targetVersion; + console.log(`\nšŸ“¦ Target version: ${newVersion}`); + } else { + newVersion = await getLatestVersion(depName); + console.log(`\nšŸ“¦ Latest version: ${newVersion}`); + } + + if (dryRun) { + console.log('\nšŸ” Dry run - no changes will be made\n'); + console.log('Would update:'); + for (const pkg of packagesWithDep) { + console.log(` ${pkg.packageName}: ${pkg.currentVersion} → ${newVersion}`); + } + console.log('\nThen run: yarn install'); + return; + } + + console.log('\nāœļø Updating package.json files...\n'); + for (const pkg of packagesWithDep) { + console.log(` Updating ${pkg.packageName}...`); + await updatePackageJson(pkg.packagePath, depName, newVersion, pkg.depType); + } + + console.log('\nāœ… Updated all package.json files'); + + await runYarnInstall(); + + console.log('\nāœ… Done!'); +} + +yargs + .command({ + command: 'upgrade', + describe: 'Upgrade a dependency across all workspace packages', + builder(a) { + return a.options({ + package: { + type: 'string', + demand: true, + describe: 'Name of the package to upgrade, e.g. @bitgo/wasm-utxo', + alias: 'p', + }, + version: { + type: 'string', + describe: 'Target version (defaults to latest from npm registry)', + alias: 'v', + }, + dryRun: { + type: 'boolean', + default: false, + describe: 'Show what would be updated without making changes', + alias: 'd', + }, + }); + }, + async handler(a) { + await cmdUpgrade(a); + }, + }) + .help() + .strict() + .demandCommand().argv;