Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: Improve the migrate tooling #24375

Merged
merged 5 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gold-waves-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---

Add support for `versions:migrate` to do code changes. Can be skipped with `--no-code-changes`
1 change: 1 addition & 0 deletions packages/cli/cli-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -622,5 +622,6 @@ Usage: backstage-cli versions:migrate [options]

Options:
--pattern <glob>
--skip-code-changes
-h, --help
```
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,10 @@ export function registerCommands(program: Command) {
'--pattern <glob>',
'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',
)
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/commands/versions/bump.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/commands/versions/bump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
178 changes: 178 additions & 0 deletions packages/cli/src/commands/versions/migrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down Expand Up @@ -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';",
);
});
});
73 changes: 55 additions & 18 deletions packages/cli/src/commands/versions/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,54 @@
*/
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' {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ew, but ok

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) {
await runYarnInstall();
}
};

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<string, string>();

for (const depType of [
'dependencies',
Expand Down Expand Up @@ -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})`,
Expand All @@ -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`,
),
);
}
}
}
}

Expand Down
Loading
Loading