diff --git a/.changeset/gold-waves-bake.md b/.changeset/gold-waves-bake.md new file mode 100644 index 0000000000..2883b7b967 --- /dev/null +++ b/.changeset/gold-waves-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add support for `versions:migrate` to do code changes. Can be skipped with `--no-code-changes` diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index a913b63f60..819b1a2261 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -622,5 +622,6 @@ Usage: backstage-cli versions:migrate [options] Options: --pattern + --skip-code-changes -h, --help ``` diff --git a/packages/cli/package.json b/packages/cli/package.json index ca061ea9c4..537aec421d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -120,6 +120,7 @@ "react-dev-utils": "^12.0.0-next.60", "react-refresh": "^0.14.0", "recursive-readdir": "^2.2.2", + "replace-in-file": "^7.1.0", "rollup": "^4.0.0", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-esbuild": "^6.1.1", diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 609c7b5946..fe23926148 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -402,6 +402,10 @@ export function registerCommands(program: Command) { '--pattern ', 'Override glob for matching packages to upgrade', ) + .option( + '--skip-code-changes', + 'Skip code changes and only update package.json files', + ) .description( 'Migrate any plugins that have been moved to the @backstage-community namespace automatically', ) diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index a6537423c8..c8174639a6 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -216,6 +216,7 @@ describe('bump', () => { 'bumping @backstage/core in b to ^1.0.6', 'bumping @backstage/theme in b to ^2.0.0', 'Running yarn install to install new versions', + 'Checking for moved packages to the @backstage-community namespace...', '⚠️ The following packages may have breaking changes:', ' @backstage/theme : 1.0.0 ~> 2.0.0', ' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md', @@ -323,6 +324,7 @@ describe('bump', () => { 'bumping @backstage/core in b to ^1.0.6', 'bumping @backstage/theme in b to ^2.0.0', 'Skipping yarn install', + 'Checking for moved packages to the @backstage-community namespace...', '⚠️ The following packages may have breaking changes:', ' @backstage/theme : 1.0.0 ~> 2.0.0', ' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md', @@ -437,6 +439,7 @@ describe('bump', () => { 'bumping @backstage/core in a to ^1.0.6', 'Your project is now at version 0.0.1, which has been written to backstage.json', 'Running yarn install to install new versions', + 'Checking for moved packages to the @backstage-community namespace...', '⚠️ The following packages may have breaking changes:', ' @backstage/theme : 1.0.0 ~> 5.0.0', ' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md', @@ -640,6 +643,7 @@ describe('bump', () => { 'bumping @backstage/core in a to ^1.0.6', 'Your project is now at version 1.0.0, which has been written to backstage.json', 'Running yarn install to install new versions', + 'Checking for moved packages to the @backstage-community namespace...', '⚠️ The following packages may have breaking changes:', ' @backstage/theme : 1.0.0 ~> 5.0.0', ' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md', @@ -745,6 +749,7 @@ describe('bump', () => { 'bumping @backstage/theme in b to ^2.0.0', 'Skipping backstage.json update as custom pattern is used', 'Running yarn install to install new versions', + 'Checking for moved packages to the @backstage-community namespace...', '⚠️ The following packages may have breaking changes:', ' @backstage-extra/custom-two : 1.0.0 ~> 2.0.0', ' @backstage/theme : 1.0.0 ~> 2.0.0', @@ -968,6 +973,7 @@ describe('bump', () => { 'bumping @backstage/core in b to ^1.0.6', 'bumping @backstage/theme in b to ^2.0.0', 'Running yarn install to install new versions', + 'Checking for moved packages to the @backstage-community namespace...', '⚠️ The following packages may have breaking changes:', ' @backstage/theme : 1.0.0 ~> 2.0.0', ' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md', diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index dc0aa27530..aad5a7831f 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -266,9 +266,12 @@ export default async (opts: OptionValues) => { } if (!opts.skipMigrate) { + console.log(); + const changed = await migrateMovedPackages({ pattern: opts.pattern, }); + if (changed && !opts.skipInstall) { await runYarnInstall(); } diff --git a/packages/cli/src/commands/versions/migrate.test.ts b/packages/cli/src/commands/versions/migrate.test.ts index 46c7f9bfb8..043e1893a4 100644 --- a/packages/cli/src/commands/versions/migrate.test.ts +++ b/packages/cli/src/commands/versions/migrate.test.ts @@ -120,6 +120,7 @@ describe('versions:migrate', () => { }); expectLogsToMatch(logs, [ + 'Checking for moved packages to the @backstage-community namespace...', 'Found a moved package @backstage/custom@^1.0.1 -> @backstage-community/custom in a (dependencies)', 'Found a moved package @backstage/custom-two@^1.0.0 -> @backstage-community/custom-two in a (dependencies)', 'Found a moved package @backstage/custom@^1.1.0 -> @backstage-community/custom in b (dependencies)', @@ -164,4 +165,181 @@ describe('versions:migrate', () => { }, }); }); + + it('should replace the occurences of the moved package in files inside the correct package', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + workspaces: { + 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({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', + '@backstage/custom': '^1.0.1', + '@backstage/custom-two': '^1.0.0', + }, + }), + src: { + 'index.ts': "import { myThing } from '@backstage/custom';", + 'index.test.ts': "import { myThing } from '@backstage/custom-two';", + }, + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', + '@backstage/theme': '^1.0.0', + '@backstage/custom': '^1.1.0', + '@backstage/custom-two': '^1.0.0', + }, + }), + }, + }, + }); + + jest.spyOn(run, 'run').mockResolvedValue(undefined); + + await withLogCollector(async () => { + await migrate({}); + }); + + expect(run.run).toHaveBeenCalledTimes(1); + expect(run.run).toHaveBeenCalledWith( + 'yarn', + ['install'], + expect.any(Object), + ); + + const indexA = await fs.readFile( + mockDir.resolve('packages/a/src/index.ts'), + 'utf-8', + ); + + expect(indexA).toEqual( + "import { myThing } from '@backstage-community/custom';", + ); + + const indexTestA = await fs.readFile( + mockDir.resolve('packages/a/src/index.test.ts'), + 'utf-8', + ); + + expect(indexTestA).toEqual( + "import { myThing } from '@backstage-community/custom-two';", + ); + }); + + it('should replaces the occurences of changed packages, and is careful', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + workspaces: { + 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', + }), + }, + }, + }, + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', + '@backstage/custom': '^1.0.1', + '@backstage/custom-two': '^1.0.0', + }, + }), + src: { + 'index.ts': "import { myThing } from '@backstage/custom';", + 'index.test.ts': "import { myThing } from '@backstage/custom-two';", + }, + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', + '@backstage/theme': '^1.0.0', + '@backstage/custom': '^1.1.0', + '@backstage/custom-two': '^1.0.0', + }, + }), + }, + }, + }); + + jest.spyOn(run, 'run').mockResolvedValue(undefined); + + await withLogCollector(async () => { + await migrate({}); + }); + + expect(run.run).toHaveBeenCalledTimes(1); + expect(run.run).toHaveBeenCalledWith( + 'yarn', + ['install'], + expect.any(Object), + ); + + const indexA = await fs.readFile( + mockDir.resolve('packages/a/src/index.ts'), + 'utf-8', + ); + + expect(indexA).toEqual( + "import { myThing } from '@backstage-community/custom';", + ); + + const indexTestA = await fs.readFile( + mockDir.resolve('packages/a/src/index.test.ts'), + 'utf-8', + ); + + expect(indexTestA).toEqual( + "import { myThing } from '@backstage/custom-two';", + ); + }); }); diff --git a/packages/cli/src/commands/versions/migrate.ts b/packages/cli/src/commands/versions/migrate.ts index f824fa2b96..321a78ef9f 100644 --- a/packages/cli/src/commands/versions/migrate.ts +++ b/packages/cli/src/commands/versions/migrate.ts @@ -15,15 +15,33 @@ */ import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; import chalk from 'chalk'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, join as joinPath } from 'path'; import { OptionValues } from 'commander'; import { readJson, writeJson } from 'fs-extra'; import { minimatch } from 'minimatch'; import { runYarnInstall } from './bump'; +import replace from 'replace-in-file'; + +declare module 'replace-in-file' { + export default function (config: { + files: string | string[]; + processor: (content: string, file: string) => string; + ignore?: string | string[]; + allowEmptyPaths?: boolean; + }): Promise< + { + file: string; + hasChanged: boolean; + numMatches?: number; + numReplacements?: number; + }[] + >; +} export default async (options: OptionValues) => { const changed = await migrateMovedPackages({ pattern: options.pattern, + skipCodeChanges: options.skipCodeChanges, }); if (changed) { @@ -31,29 +49,20 @@ export default async (options: OptionValues) => { } }; -export async function migrateMovedPackages(options?: { pattern?: string }) { +export async function migrateMovedPackages(options?: { + pattern?: string; + skipCodeChanges?: boolean; +}) { + console.log( + 'Checking for moved packages to the @backstage-community namespace...', + ); 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; + const movedPackages = new Map(); for (const depType of [ 'dependencies', @@ -87,6 +96,7 @@ export async function migrateMovedPackages(options?: { pattern?: string }) { const movedPackageName = packageInfo.backstage?.moved; if (movedPackageName) { + movedPackages.set(depName, movedPackageName); console.log( chalk.yellow( `Found a moved package ${depName}@${depVersion} -> ${movedPackageName} in ${pkgName} (${depType})`, @@ -106,6 +116,33 @@ export async function migrateMovedPackages(options?: { pattern?: string }) { await writeJson(resolvePath(pkg.dir, 'package.json'), pkg.packageJson, { spaces: 2, }); + + if (!options?.skipCodeChanges) { + // Replace all occurrences of the old package names in the code. + const files = await replace({ + files: joinPath(pkg.dir, 'src', '**'), + allowEmptyPaths: true, + processor: content => { + return Array.from(movedPackages.entries()).reduce( + (newContent, [oldName, newName]) => { + return newContent + .replace(new RegExp(`"${oldName}"`, 'g'), `"${newName}"`) + .replace(new RegExp(`'${oldName}'`, 'g'), `'${newName}'`) + .replace(new RegExp(`${oldName}/`, 'g'), `${newName}/`); + }, + content, + ); + }, + }); + + if (files.length > 0) { + console.log( + chalk.green( + `Updated ${files.length} files in ${pkgName} to use the new package names`, + ), + ); + } + } } } diff --git a/yarn.lock b/yarn.lock index ad5d48bd8f..0a27eecdea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3729,6 +3729,7 @@ __metadata: react-dev-utils: ^12.0.0-next.60 react-refresh: ^0.14.0 recursive-readdir: ^2.2.2 + replace-in-file: ^7.1.0 rollup: ^4.0.0 rollup-plugin-dts: ^6.1.0 rollup-plugin-esbuild: ^6.1.1 @@ -29082,7 +29083,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.0, glob@npm:^8.0.1, glob@npm:^8.0.3": +"glob@npm:^8.0.0, glob@npm:^8.0.1, glob@npm:^8.0.3, glob@npm:^8.1.0": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -40432,6 +40433,19 @@ __metadata: languageName: node linkType: hard +"replace-in-file@npm:^7.1.0": + version: 7.1.0 + resolution: "replace-in-file@npm:7.1.0" + dependencies: + chalk: ^4.1.2 + glob: ^8.1.0 + yargs: ^17.7.2 + bin: + replace-in-file: bin/cli.js + checksum: 2ed61bd0cf0752b18775b52342ad36f4ee6c806f7eca0b0d085c23bafe0cb4828e4ec8f59058bde6b67d2ed5ac51e1681284f586089b58b966e2489712830db0 + languageName: node + linkType: hard + "request@npm:^2.88.0": version: 2.88.2 resolution: "request@npm:2.88.2"