cli: use backstage:^ versions when yarn plugin is installed

Introduces basic support for backstage:^ versions
in the versions:bump command in backstage-cli. The
behavior changes in the following ways:

1. If the yarn plugin is detected, write
    backstage:^ versions in package.json files in
    the repo where appropriate.
2. Update breaking change detection to
    correctly resolve lockfile entries for
    packages using backstage:^ versions.

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2024-04-22 22:22:46 +01:00
parent 329b1ea357
commit 3d88455be9
4 changed files with 92 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Add support for `backstage:^` version ranges to versions:bump when using the experimental yarn plugin
+1
View File
@@ -117,6 +117,7 @@
"jest-css-modules": "^2.1.0",
"jest-environment-jsdom": "^29.0.2",
"jest-runtime": "^29.0.2",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.4.2",
+85 -6
View File
@@ -25,6 +25,8 @@ import chalk from 'chalk';
import ora from 'ora';
import semver from 'semver';
import { OptionValues } from 'commander';
import yaml from 'js-yaml';
import z from 'zod';
import { isError, NotFoundError } from '@backstage/errors';
import { resolve as resolvePath } from 'path';
import { run } from '../../lib/run';
@@ -76,6 +78,14 @@ type PkgVersionInfo = {
export default async (opts: OptionValues) => {
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
const hasYarnPlugin = await getHasYarnPlugin();
if (hasYarnPlugin) {
console.log(
`Backstage yarn plugin detected, will use backstage: version ranges where possible...`,
);
}
let pattern = opts.pattern;
if (!pattern) {
@@ -177,12 +187,27 @@ export default async (opts: OptionValues) => {
for (const depType of DEP_TYPES) {
if (depType in pkgJson && dep.name in pkgJson[depType]) {
const oldRange = pkgJson[depType][dep.name];
pkgJson[depType][dep.name] = dep.range;
// backstage:^ are written to the lockfile as
// backstage:<backstage-version>, so that updates to
// backstage.json can be detected during yarn install. In order to
// locate the corresponding lockfile entry for "backstage:^"
// versions, we need to perform the same transformation.
const oldLockfileRange = await asLockfileVersion(oldRange);
// Don't use backstage:^ versions for peerDependencies; they only
// support npm and workspace: versions.
const useBackstageRange =
hasYarnPlugin && depType !== 'peerDependencies';
const newRange = useBackstageRange ? 'backstage:^' : dep.range;
pkgJson[depType][dep.name] = newRange;
// Check if the update was at least a pre-v1 minor or post-v1 major release
const lockfileEntry = lockfile
.get(dep.name)
?.find(entry => entry.range === oldRange);
?.find(entry => entry.range === oldLockfileRange);
if (lockfileEntry) {
const from = lockfileEntry.version;
const to = dep.target;
@@ -354,16 +379,23 @@ export function createVersionFinder(options: {
};
}
export async function bumpBackstageJsonVersion(version: string) {
const backstageJsonPath = paths.resolveTargetRoot(BACKSTAGE_JSON);
const backstageJson = await fs.readJSON(backstageJsonPath).catch(e => {
function getBackstageJsonPath() {
return paths.resolveTargetRoot(BACKSTAGE_JSON);
}
async function getBackstageJson() {
const backstageJsonPath = getBackstageJsonPath();
return fs.readJSON(backstageJsonPath).catch(e => {
if (e.code === 'ENOENT') {
// gracefully continue in case the file doesn't exist
return;
}
throw e;
});
}
export async function bumpBackstageJsonVersion(version: string) {
const backstageJson = await getBackstageJson();
const prevVersion = backstageJson?.version;
if (prevVersion === version) {
@@ -394,7 +426,7 @@ export async function bumpBackstageJsonVersion(version: string) {
}
await fs.writeJson(
backstageJsonPath,
getBackstageJsonPath(),
{ ...backstageJson, version },
{
spaces: 2,
@@ -403,6 +435,53 @@ export async function bumpBackstageJsonVersion(version: string) {
);
}
async function asLockfileVersion(version: string) {
if (version === 'backstage:^') {
return `backstage:${(await getBackstageJson())?.version}`;
}
return version;
}
const yarnRcSchema = z.object({
plugins: z
.array(
z.object({
path: z.string(),
}),
)
.optional(),
});
async function getHasYarnPlugin() {
const yarnRcPath = paths.resolveTargetRoot('.yarnrc.yml');
const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => {
if (e.code === 'ENOENT') {
// gracefully continue in case the file doesn't exist
return '';
}
throw e;
});
if (!yarnRcContent) {
return false;
}
const parseResult = yarnRcSchema.safeParse(yaml.load(yarnRcContent));
if (!parseResult.success) {
throw new Error(
`Unexpected content in .yarnrc.yml: ${parseResult.error.toString()}`,
);
}
const yarnRc = parseResult.data;
return yarnRc.plugins?.some(
plugin => plugin.path === '.yarn/plugins/@yarnpkg/plugin-backstage.cjs',
);
}
export async function runYarnInstall() {
const spinner = ora({
prefixText: `Running ${chalk.blue('yarn install')} to install new versions`,
+1
View File
@@ -3993,6 +3993,7 @@ __metadata:
jest-css-modules: ^2.1.0
jest-environment-jsdom: ^29.0.2
jest-runtime: ^29.0.2
js-yaml: ^4.1.0
json-schema: ^0.4.0
lodash: ^4.17.21
mini-css-extract-plugin: ^2.4.2