diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index 86d54bf328..68ba614c94 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -45,6 +45,7 @@ export interface BackstagePackageJson { backstage?: { role?: PackageRole; + moved?: string; }; exports?: JsonValue; @@ -136,8 +137,8 @@ export class PackageGraph extends Map { * Lists all local packages in a monorepo. */ static async listTargetPackages(): Promise { - console.log(paths.targetDir); const { packages } = await getPackages(paths.targetDir); + return packages as BackstagePackage[]; } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index a33394b414..98ade80472 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -397,6 +397,10 @@ export function registerCommands(program: Command) { program .command('versions:migrate') + .option( + '--pattern ', + 'Override glob for matching packages to upgrade', + ) .description( 'Migrate any plugins that have been moved to the @backstage-community namespace automatically', ) diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index d64d5a9756..3d3ab16427 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -470,7 +470,7 @@ export async function bumpBackstageJsonVersion(version: string) { ); } -async function runYarnInstall() { +export async function runYarnInstall() { const spinner = ora({ prefixText: `Running ${chalk.blue('yarn install')} to install new versions`, spinner: 'arc', diff --git a/packages/cli/src/commands/versions/migrate.test.ts b/packages/cli/src/commands/versions/migrate.test.ts index c822b98e9c..878906a1f0 100644 --- a/packages/cli/src/commands/versions/migrate.test.ts +++ b/packages/cli/src/commands/versions/migrate.test.ts @@ -43,36 +43,9 @@ jest.mock('../../lib/run', () => { }; }); -const mockFetchPackageInfo = jest.fn(); -jest.mock('../../lib/versioning/packages', () => { - const actual = jest.requireActual('../../lib/versioning/packages'); - return { - ...actual, - fetchPackageInfo: (name: string) => mockFetchPackageInfo(name), - }; -}); - -const REGISTRY_VERSIONS: { [name: string]: string } = { - '@backstage/core': '1.0.6', - '@backstage/core-api': '1.0.7', - '@backstage/theme': '2.0.0', - '@backstage-extra/custom': '1.1.0', - '@backstage-extra/custom-two': '2.0.0', - '@backstage/create-app': '1.0.0', -}; - describe('versions:migrate', () => { mockDir = createMockDirectory(); - beforeEach(() => { - mockFetchPackageInfo.mockImplementation(async name => ({ - name: name, - 'dist-tags': { - latest: REGISTRY_VERSIONS[name], - }, - })); - }); - afterEach(() => { jest.resetAllMocks(); }); @@ -84,6 +57,28 @@ describe('versions:migrate', () => { packages: ['packages/*'], }, }), + node_modules: { + '@backstage': { + custom: { + 'package.json': JSON.stringify({ + name: '@backstage-extra/custom', + version: '1.0.1', + backstage: { + moved: '@backstage-community/custom', + }, + }), + }, + 'custom-two': { + 'package.json': JSON.stringify({ + name: '@backstage-extra/custom-two', + version: '1.0.0', + backstage: { + moved: '@backstage-community/custom-two', + }, + }), + }, + }, + }, packages: { a: { 'package.json': JSON.stringify({ @@ -110,7 +105,6 @@ describe('versions:migrate', () => { }); jest.spyOn(run, 'run').mockResolvedValue(undefined); - console.log(paths); await migrate({}); const { log: logs } = await withLogCollector(['log'], async () => {}); @@ -141,10 +135,6 @@ describe('versions:migrate', () => { // 'Version bump complete!', // ]); - expect(mockFetchPackageInfo).toHaveBeenCalledTimes(5); - expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core'); - expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/theme'); - expect(run.run).toHaveBeenCalledTimes(1); expect(run.run).toHaveBeenCalledWith( 'yarn', @@ -155,12 +145,13 @@ describe('versions:migrate', () => { const packageA = await fs.readJson( mockDir.resolve('packages/a/package.json'), ); + expect(packageA).toEqual({ name: 'a', dependencies: { - '@backstage-community/custom': '^1.1.0', - '@backstage-community/custom-two': '^2.0.0', - '@backstage/core': '^1.0.6', + '@backstage-community/custom': '^1.0.1', + '@backstage-community/custom-two': '^1.0.0', + '@backstage/core': '^1.0.5', }, }); const packageB = await fs.readJson( @@ -170,9 +161,9 @@ describe('versions:migrate', () => { name: 'b', dependencies: { '@backstage-community/custom': '^1.1.0', - '@backstage-community/custom-two': '^2.0.0', - '@backstage/core': '^1.0.6', - '@backstage/theme': '^2.0.0', + '@backstage-community/custom-two': '^1.0.0', + '@backstage/core': '^1.0.3', + '@backstage/theme': '^1.0.0', }, }); }); diff --git a/packages/cli/src/commands/versions/migrate.ts b/packages/cli/src/commands/versions/migrate.ts index 557680d5e3..fa3492ca7c 100644 --- a/packages/cli/src/commands/versions/migrate.ts +++ b/packages/cli/src/commands/versions/migrate.ts @@ -13,17 +13,97 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PackageGraph } from '@backstage/cli-node'; +import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; +import chalk from 'chalk'; +import { resolve as resolvePath } from 'path'; import { OptionValues } from 'commander'; +import { readJson, writeJson } from 'fs-extra'; +import { minimatch } from 'minimatch'; +import { runYarnInstall } from './bump'; -export default async (_: OptionValues) => { - const packageMap = PackageGraph.fromPackages( - await PackageGraph.listTargetPackages(), - ); +export default async (options: OptionValues) => { + const changed = await migrateMovedPackages({ + pattern: options.pattern, + }); - const packagesThatHaveMoved = new Map(); - - for (const [name, pkg] of packageMap.entries()) { - console.log(name, pkg); + if (changed) { + await runYarnInstall(); } }; + +export async function migrateMovedPackages(options?: { pattern?: string }) { + const packages = await PackageGraph.listTargetPackages(); + + const thingsThatHaveMoved = new Map< + string, + { + dependencies: { [k: string]: string }; + devDependencies: { [k: string]: string }; + peerDependencies: { [k: string]: string }; + } + >(); + + let didAnythingChange = false; + + for (const pkg of packages) { + const pkgName = pkg.packageJson.name; + thingsThatHaveMoved.set(pkgName, { + dependencies: {}, + devDependencies: {}, + peerDependencies: {}, + }); + + let didPackageChange = false; + + for (const depType of [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + ] as const) { + const depsObj = pkg.packageJson[depType]; + if (!depsObj) { + continue; + } + for (const [depName, depVersion] of Object.entries(depsObj)) { + if (options?.pattern && !minimatch(depName, options.pattern)) { + continue; + } + let packageInfo: BackstagePackageJson; + try { + packageInfo = await readJson( + require.resolve(`${depName}/package.json`, { + paths: [pkg.dir], + }), + ); + } catch (ex) { + console.warn( + chalk.yellow`Could not find package.json for ${depName}@${depVersion} in ${pkgName} (${depType})`, + ); + continue; + } + + const movedPackageName = packageInfo.backstage?.moved; + + if (movedPackageName) { + console.log( + chalk.yellow`Found a moved package ${depName}@${depVersion} -> ${movedPackageName} in ${pkgName} (${depType})`, + ); + + didPackageChange = true; + didAnythingChange = true; + + depsObj[movedPackageName] = depsObj[depName]; + delete depsObj[depName]; + } + } + } + + if (didPackageChange) { + await writeJson(resolvePath(pkg.dir, 'package.json'), pkg.packageJson, { + spaces: 2, + }); + } + } + + return didAnythingChange; +}