Split CLI modules into separate packages
Extract each CLI module from packages/cli/src/modules/ into its own package under packages/cli-module-*. This enables independent versioning and clearer dependency boundaries for each CLI capability. Module mapping: - auth → @backstage/cli-module-auth - build → @backstage/cli-module-build - config → @backstage/cli-module-config - create-github-app → @backstage/cli-module-create-github-app - info → @backstage/cli-module-info - lint → @backstage/cli-module-lint - maintenance → @backstage/cli-module-maintenance - migrate → @backstage/cli-module-migrate - new → @backstage/cli-module-new - test → @backstage/cli-module-test-jest - translations → @backstage/cli-module-translations Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-cli-module-migrate
|
||||
title: '@backstage/cli-module-migrate'
|
||||
description: CLI module for Backstage CLI
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-cli-module
|
||||
owner: tooling-maintainers
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@backstage/cli-module-migrate",
|
||||
"version": "0.1.0",
|
||||
"description": "CLI module for Backstage CLI",
|
||||
"backstage": {
|
||||
"role": "cli-module"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/cli-module-migrate"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "workspace:^",
|
||||
"@backstage/cli-node": "workspace:^",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"cleye": "^2.3.0",
|
||||
"fs-extra": "^11.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@types/fs-extra": "^11.0.0",
|
||||
"msw": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
cli({ help: info, booleanFlagNegation: true }, undefined, args);
|
||||
throw new Error(
|
||||
'The `migrate package-exports` command has been removed, use `repo fix` instead.',
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
import { runOutput } from '@backstage/cli-common';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`;
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
cli({ help: info, booleanFlagNegation: true }, undefined, args);
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const oldConfigs = [
|
||||
require.resolve('@backstage/cli/config/eslint.js'),
|
||||
require.resolve('@backstage/cli/config/eslint.backend.js'),
|
||||
];
|
||||
|
||||
const configPaths = new Array<string>();
|
||||
await Promise.all(
|
||||
packages.map(async ({ dir, packageJson }) => {
|
||||
const configPath = resolvePath(dir, '.eslintrc.js');
|
||||
if (!(await fs.pathExists(configPath))) {
|
||||
console.log(`Skipping ${packageJson.name}, missing .eslintrc.js`);
|
||||
return;
|
||||
}
|
||||
let existingConfig: Record<string, unknown>;
|
||||
try {
|
||||
existingConfig = require(configPath);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Skipping ${packageJson.name}, failed to load .eslintrc.js, ${error}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only transform configs that extend the old config files, and
|
||||
// we remove that entry from the extends array.
|
||||
const extendsArray = (existingConfig.extends as string[]) ?? [];
|
||||
const extendIndex = extendsArray.findIndex(p => oldConfigs.includes(p));
|
||||
if (extendIndex === -1) {
|
||||
console.log(
|
||||
`Skipping ${packageJson.name}, .eslintrc.js does not extend the legacy config`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
extendsArray.splice(extendIndex, 1);
|
||||
if (extendsArray.length === 0) {
|
||||
delete existingConfig.extends;
|
||||
}
|
||||
|
||||
if (Object.keys(existingConfig).length > 0) {
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${PREFIX}(__dirname, ${JSON.stringify(existingConfig, null, 2)});\n`,
|
||||
);
|
||||
} else {
|
||||
await fs.writeFile(configPath, `${PREFIX}(__dirname);\n`);
|
||||
}
|
||||
configPaths.push(configPath);
|
||||
}),
|
||||
);
|
||||
|
||||
// If prettier is present, then we run that too
|
||||
let hasPrettier = false;
|
||||
try {
|
||||
require.resolve('prettier');
|
||||
hasPrettier = true;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
if (hasPrettier) {
|
||||
await runOutput(['prettier', '--write', ...configPaths]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { getPackages } from '@manypkg/get-packages';
|
||||
import { PackageRoles } from '@backstage/cli-node';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
cli({ help: info, booleanFlagNegation: true }, undefined, args);
|
||||
const { packages } = await getPackages(targetPaths.dir);
|
||||
|
||||
await Promise.all(
|
||||
packages.map(async ({ dir, packageJson: pkg }) => {
|
||||
const { name } = pkg;
|
||||
const existingRole = PackageRoles.getRoleFromPackage(pkg);
|
||||
if (existingRole) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detectedRole = PackageRoles.detectRoleFromPackage(pkg);
|
||||
if (!detectedRole) {
|
||||
console.error(`No role detected for package ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Detected package role of ${name} as ${detectedRole}`);
|
||||
|
||||
let newPkg = pkg as any;
|
||||
|
||||
const pkgKeys = Object.keys(pkg);
|
||||
if (pkgKeys.includes('backstage')) {
|
||||
newPkg.backstage = {
|
||||
...newPkg.backstage,
|
||||
role: detectedRole,
|
||||
};
|
||||
} else {
|
||||
// We insert the backstage field after one of these fields, otherwise at the end
|
||||
const index =
|
||||
Math.max(
|
||||
pkgKeys.indexOf('version'),
|
||||
pkgKeys.indexOf('private'),
|
||||
pkgKeys.indexOf('publishConfig'),
|
||||
) + 1 || pkgKeys.length;
|
||||
|
||||
const pkgEntries = Object.entries(pkg);
|
||||
pkgEntries.splice(index, 0, ['backstage', { role: detectedRole }]);
|
||||
newPkg = Object.fromEntries(pkgEntries);
|
||||
}
|
||||
|
||||
await fs.writeJson(resolvePath(dir, 'package.json'), newPkg, {
|
||||
spaces: 2,
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { PackageGraph, PackageRoles, PackageRole } from '@backstage/cli-node';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
const configArgPattern = /--config[=\s][^\s$]+/;
|
||||
|
||||
const noStartRoles: PackageRole[] = ['cli', 'cli-module', 'common-library'];
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
cli({ help: info, booleanFlagNegation: true }, undefined, args);
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await Promise.all(
|
||||
packages.map(async ({ dir, packageJson }) => {
|
||||
const role = PackageRoles.getRoleFromPackage(packageJson);
|
||||
if (!role) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleInfo = PackageRoles.getRoleInfo(role);
|
||||
const hasStart = !noStartRoles.includes(role);
|
||||
const needsPack = !(roleInfo.output.includes('bundle') || role === 'cli');
|
||||
const scripts = packageJson.scripts ?? {};
|
||||
|
||||
const startCmd = ['start'];
|
||||
if (scripts.start?.includes('--check')) {
|
||||
startCmd.push('--check');
|
||||
}
|
||||
if (scripts.start?.includes('--config')) {
|
||||
startCmd.push(...(scripts.start.match(configArgPattern) ?? []));
|
||||
}
|
||||
|
||||
const buildCmd = ['build'];
|
||||
if (scripts.build?.includes('--minify')) {
|
||||
buildCmd.push('--minify');
|
||||
}
|
||||
if (scripts.build?.includes('--config')) {
|
||||
buildCmd.push(...(scripts.build.match(configArgPattern) ?? []));
|
||||
}
|
||||
|
||||
// For test scripts we keep all existing flags except for --passWithNoTests, since that's now default
|
||||
const testCmd = ['test'];
|
||||
if (scripts.test?.startsWith('backstage-cli test')) {
|
||||
const testArgs = scripts.test
|
||||
.slice('backstage-cli test'.length)
|
||||
.split(' ')
|
||||
.filter(Boolean);
|
||||
if (testArgs.includes('--passWithNoTests')) {
|
||||
testArgs.splice(testArgs.indexOf('--passWithNoTests'), 1);
|
||||
}
|
||||
testCmd.push(...testArgs);
|
||||
}
|
||||
|
||||
const expectedScripts = {
|
||||
...(hasStart && {
|
||||
start: `backstage-cli package ${startCmd.join(' ')}`,
|
||||
}),
|
||||
build: `backstage-cli package ${buildCmd.join(' ')}`,
|
||||
lint: 'backstage-cli package lint',
|
||||
test: `backstage-cli package ${testCmd.join(' ')}`,
|
||||
clean: 'backstage-cli package clean',
|
||||
...(needsPack && {
|
||||
postpack: 'backstage-cli package postpack',
|
||||
prepack: 'backstage-cli package prepack',
|
||||
}),
|
||||
};
|
||||
|
||||
let changed = false;
|
||||
const currentScripts: Record<string, string | undefined> =
|
||||
(packageJson.scripts = packageJson.scripts || {});
|
||||
|
||||
for (const [name, value] of Object.entries(expectedScripts)) {
|
||||
const currentScript = currentScripts[name];
|
||||
|
||||
const isMissing = !currentScript;
|
||||
const isDifferent = currentScript !== value;
|
||||
const isBackstageScript = currentScript?.includes('backstage-cli');
|
||||
if (isMissing || (isDifferent && isBackstageScript)) {
|
||||
changed = true;
|
||||
currentScripts[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
console.log(`Updating scripts for ${packageJson.name}`);
|
||||
await fs.writeJson(resolvePath(dir, 'package.json'), packageJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { PackageGraph, PackageRoles } from '@backstage/cli-node';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
const REACT_ROUTER_DEPS = ['react-router', 'react-router-dom'];
|
||||
const REACT_ROUTER_RANGE = '6.0.0-beta.0 || ^6.3.0';
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
cli({ help: info, booleanFlagNegation: true }, undefined, args);
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await Promise.all(
|
||||
packages.map(async ({ dir, packageJson }) => {
|
||||
const role = PackageRoles.getRoleFromPackage(packageJson);
|
||||
if (role === 'frontend') {
|
||||
console.log(`Skipping ${packageJson.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
for (const depName of ['dependencies', 'devDependencies'] as const) {
|
||||
const depsCollection = packageJson[depName];
|
||||
if (depsCollection) {
|
||||
for (const key of Object.keys(depsCollection)) {
|
||||
if (REACT_ROUTER_DEPS.includes(key)) {
|
||||
delete depsCollection[key];
|
||||
const peerDeps = (packageJson.peerDependencies =
|
||||
packageJson.peerDependencies ?? {});
|
||||
peerDeps[key] = REACT_ROUTER_RANGE;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
console.log(`Updating dependencies for ${packageJson.name}`);
|
||||
await fs.writeJson(resolvePath(dir, 'package.json'), packageJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,526 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
BACKSTAGE_JSON,
|
||||
bootstrapEnvProxyAgents,
|
||||
targetPaths,
|
||||
} from '@backstage/cli-common';
|
||||
|
||||
bootstrapEnvProxyAgents();
|
||||
|
||||
import { env } from 'node:process';
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import { minimatch } from 'minimatch';
|
||||
import semver from 'semver';
|
||||
import { cli } from 'cleye';
|
||||
import { isError, NotFoundError } from '@backstage/errors';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
|
||||
import {
|
||||
hasBackstageYarnPlugin,
|
||||
Lockfile,
|
||||
runConcurrentTasks,
|
||||
} from '@backstage/cli-node';
|
||||
import {
|
||||
fetchPackageInfo,
|
||||
mapDependencies,
|
||||
YarnInfoInspectData,
|
||||
} from '../../lib/versioning/packages';
|
||||
import {
|
||||
getManifestByReleaseLine,
|
||||
getManifestByVersion,
|
||||
ReleaseManifest,
|
||||
} from '@backstage/release-manifests';
|
||||
import { migrateMovedPackages } from './migrate';
|
||||
import { runYarnInstall } from '../../lib/utils';
|
||||
import { run } from '@backstage/cli-common';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
const DEP_TYPES = [
|
||||
'dependencies',
|
||||
'devDependencies',
|
||||
'peerDependencies',
|
||||
'optionalDependencies',
|
||||
];
|
||||
|
||||
const DEFAULT_PATTERN_GLOB = '@backstage/*';
|
||||
|
||||
type PkgVersionInfo = {
|
||||
range: string;
|
||||
target: string;
|
||||
name: string;
|
||||
location: string;
|
||||
};
|
||||
|
||||
function extendsDefaultPattern(pattern: string): boolean {
|
||||
if (!pattern.endsWith('/*')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return minimatch('@backstage/', pattern.slice(0, -1));
|
||||
}
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const {
|
||||
flags: { pattern: patternFlag, release, skipInstall, skipMigrate },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
booleanFlagNegation: true,
|
||||
flags: {
|
||||
pattern: {
|
||||
type: String,
|
||||
description: 'Override glob for matching packages to upgrade',
|
||||
},
|
||||
release: {
|
||||
type: String,
|
||||
description: 'Bump to a specific Backstage release line or version',
|
||||
default: 'main',
|
||||
},
|
||||
skipInstall: {
|
||||
type: Boolean,
|
||||
description: 'Skips yarn install step',
|
||||
},
|
||||
skipMigrate: {
|
||||
type: Boolean,
|
||||
description: 'Skips migration of any moved packages',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const lockfilePath = targetPaths.resolveRoot('yarn.lock');
|
||||
const lockfile = await Lockfile.load(lockfilePath);
|
||||
const yarnPluginEnabled = await hasBackstageYarnPlugin();
|
||||
|
||||
let pattern = patternFlag;
|
||||
|
||||
if (!pattern) {
|
||||
console.log(`Using default pattern glob ${DEFAULT_PATTERN_GLOB}`);
|
||||
pattern = DEFAULT_PATTERN_GLOB;
|
||||
} else if (pattern === '*') {
|
||||
throw new Error(`Rejected pattern '*', please use a more specific pattern`);
|
||||
} else {
|
||||
console.log(`Using custom pattern glob ${pattern}`);
|
||||
}
|
||||
|
||||
let findTargetVersion: (name: string) => Promise<string>;
|
||||
let releaseManifest: ReleaseManifest;
|
||||
if (env.BACKSTAGE_MANIFEST_FILE) {
|
||||
// Use specific manifest file if provided
|
||||
releaseManifest = await fs.readJson(env.BACKSTAGE_MANIFEST_FILE);
|
||||
findTargetVersion = createStrictVersionFinder({
|
||||
releaseManifest,
|
||||
});
|
||||
} else if (semver.valid(release)) {
|
||||
// Specific release specified. Be strict when resolving versions
|
||||
releaseManifest = await getManifestByVersion({ version: release! });
|
||||
findTargetVersion = createStrictVersionFinder({
|
||||
releaseManifest,
|
||||
});
|
||||
} else {
|
||||
// Release line specified. Be lenient when resolving versions.
|
||||
if (release === 'next') {
|
||||
const next = await getManifestByReleaseLine({
|
||||
releaseLine: 'next',
|
||||
versionsBaseUrl: env.BACKSTAGE_VERSIONS_BASE_URL,
|
||||
});
|
||||
const main = await getManifestByReleaseLine({
|
||||
releaseLine: 'main',
|
||||
versionsBaseUrl: env.BACKSTAGE_VERSIONS_BASE_URL,
|
||||
});
|
||||
// Prefer manifest with the latest release version
|
||||
releaseManifest = semver.gt(next.releaseVersion, main.releaseVersion)
|
||||
? next
|
||||
: main;
|
||||
} else {
|
||||
releaseManifest = await getManifestByReleaseLine({
|
||||
releaseLine: release!,
|
||||
versionsBaseUrl: env.BACKSTAGE_VERSIONS_BASE_URL,
|
||||
});
|
||||
}
|
||||
findTargetVersion = createVersionFinder({
|
||||
releaseLine: release,
|
||||
releaseManifest,
|
||||
});
|
||||
}
|
||||
|
||||
if (yarnPluginEnabled) {
|
||||
console.log();
|
||||
console.log(
|
||||
`Updating yarn plugin to v${releaseManifest.releaseVersion}...`,
|
||||
);
|
||||
console.log();
|
||||
|
||||
const yarnPluginUrl = env.BACKSTAGE_VERSIONS_BASE_URL
|
||||
? `${env.BACKSTAGE_VERSIONS_BASE_URL}/v1/releases/${releaseManifest.releaseVersion}/yarn-plugin`
|
||||
: `https://versions.backstage.io/v1/releases/${releaseManifest.releaseVersion}/yarn-plugin`;
|
||||
|
||||
await run(['yarn', 'plugin', 'import', yarnPluginUrl]).waitForExit();
|
||||
console.log();
|
||||
}
|
||||
|
||||
// First we discover all Backstage dependencies within our own repo
|
||||
const dependencyMap = await mapDependencies(targetPaths.dir, pattern);
|
||||
|
||||
// Next check with the package registry to see which dependency ranges we need to bump
|
||||
const versionBumps = new Map<string, PkgVersionInfo[]>();
|
||||
|
||||
await runConcurrentTasks({
|
||||
concurrencyFactor: 4,
|
||||
items: dependencyMap.entries(),
|
||||
async worker([name, pkgs]) {
|
||||
let target: string;
|
||||
try {
|
||||
target = await findTargetVersion(name);
|
||||
} catch (error) {
|
||||
if (isError(error) && error.name === 'NotFoundError') {
|
||||
console.log(`Package info not found, ignoring package ${name}`);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const pkg of pkgs) {
|
||||
versionBumps.set(
|
||||
pkg.name,
|
||||
(versionBumps.get(pkg.name) ?? []).concat({
|
||||
name,
|
||||
location: pkg.location,
|
||||
range: `^${target}`, // TODO(Rugvip): Option to use something else than ^?
|
||||
target,
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Write all discovered version bumps to package.json in this repo
|
||||
if (versionBumps.size === 0) {
|
||||
console.log(chalk.green('All Backstage packages are up to date!'));
|
||||
} else {
|
||||
console.log(chalk.yellow('Some packages are outdated, updating'));
|
||||
console.log();
|
||||
|
||||
const breakingUpdates = new Map<string, { from: string; to: string }>();
|
||||
await runConcurrentTasks({
|
||||
concurrencyFactor: 4,
|
||||
items: versionBumps.entries(),
|
||||
async worker([name, deps]) {
|
||||
const pkgPath = resolvePath(deps[0].location, 'package.json');
|
||||
const pkgJson = await fs.readJson(pkgPath);
|
||||
|
||||
for (const dep of deps) {
|
||||
console.log(
|
||||
`${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan(
|
||||
name,
|
||||
)} to ${chalk.yellow(dep.range)}`,
|
||||
);
|
||||
|
||||
for (const depType of DEP_TYPES) {
|
||||
if (depType in pkgJson && dep.name in pkgJson[depType]) {
|
||||
const oldRange = pkgJson[depType][dep.name];
|
||||
|
||||
// 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);
|
||||
|
||||
const useBackstageRange =
|
||||
yarnPluginEnabled &&
|
||||
// Only use backstage:^ versions if the package is present in
|
||||
// the manifest for the release we're bumping to.
|
||||
releaseManifest.packages.find(
|
||||
({ name: manifestPackageName }) =>
|
||||
dep.name === manifestPackageName,
|
||||
) &&
|
||||
// Don't use backstage:^ versions for peerDependencies; they only
|
||||
// support npm and workspace: versions.
|
||||
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 === oldLockfileRange);
|
||||
if (lockfileEntry) {
|
||||
const from = lockfileEntry.version;
|
||||
const to = dep.target;
|
||||
if (!semver.satisfies(to, `^${from}`)) {
|
||||
breakingUpdates.set(dep.name, { from, to });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeJson(pkgPath, pkgJson, { spaces: 2 });
|
||||
},
|
||||
});
|
||||
|
||||
console.log();
|
||||
|
||||
// Do not update backstage.json when default pattern is not covered
|
||||
if (extendsDefaultPattern(pattern)) {
|
||||
await bumpBackstageJsonVersion(
|
||||
releaseManifest.releaseVersion,
|
||||
yarnPluginEnabled,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Skipping backstage.json update as custom pattern is used`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!skipInstall) {
|
||||
await runYarnInstall();
|
||||
} else {
|
||||
console.log();
|
||||
|
||||
console.log(chalk.yellow(`Skipping yarn install`));
|
||||
}
|
||||
|
||||
if (!skipMigrate) {
|
||||
console.log();
|
||||
|
||||
const changed = await migrateMovedPackages({
|
||||
pattern: patternFlag,
|
||||
});
|
||||
|
||||
if (changed && !skipInstall) {
|
||||
await runYarnInstall();
|
||||
}
|
||||
}
|
||||
|
||||
if (breakingUpdates.size > 0) {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.yellow('⚠️ The following packages may have breaking changes:'),
|
||||
);
|
||||
console.log();
|
||||
|
||||
for (const [name, { from, to }] of Array.from(
|
||||
breakingUpdates.entries(),
|
||||
).sort()) {
|
||||
console.log(
|
||||
` ${chalk.yellow(name)} : ${chalk.yellow(from)} ~> ${chalk.yellow(
|
||||
to,
|
||||
)}`,
|
||||
);
|
||||
|
||||
let path;
|
||||
if (name.startsWith('@backstage/plugin-')) {
|
||||
path = `plugins/${name.replace('@backstage/plugin-', '')}`;
|
||||
} else if (name.startsWith('@backstage/')) {
|
||||
path = `packages/${name.replace('@backstage/', '')}`;
|
||||
}
|
||||
if (path) {
|
||||
// TODO(Rugvip): Grab these URLs and paths from package.json, possibly verify existence
|
||||
// Possibly invent new "changelog" field in package.json or some sh*t.
|
||||
console.log(
|
||||
` https://github.com/backstage/backstage/blob/master/${path}/CHANGELOG.md`,
|
||||
);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
} else {
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (yarnPluginEnabled) {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.blue(
|
||||
`${chalk.bold(
|
||||
'NOTE',
|
||||
)}: this bump used backstage:^ versions in package.json files, since the Backstage ` +
|
||||
`yarn plugin was detected in the repository. To migrate back to explicit npm versions, ` +
|
||||
`remove the plugin by running "yarn plugin remove @yarnpkg/plugin-backstage", then ` +
|
||||
`repeat this command.`,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(chalk.green('Version bump complete!'));
|
||||
}
|
||||
|
||||
console.log();
|
||||
};
|
||||
|
||||
export function createStrictVersionFinder(options: {
|
||||
releaseManifest: ReleaseManifest;
|
||||
}) {
|
||||
const releasePackages = new Map(
|
||||
options.releaseManifest.packages.map(p => [p.name, p.version]),
|
||||
);
|
||||
return async function findTargetVersion(name: string) {
|
||||
console.log(`Checking for updates of ${name}`);
|
||||
const manifestVersion = releasePackages.get(name);
|
||||
if (manifestVersion) {
|
||||
return manifestVersion;
|
||||
}
|
||||
throw new NotFoundError(`Package ${name} not found in release manifest`);
|
||||
};
|
||||
}
|
||||
|
||||
export function createVersionFinder(options: {
|
||||
releaseLine?: string;
|
||||
packageInfoFetcher?: () => Promise<YarnInfoInspectData>;
|
||||
releaseManifest?: ReleaseManifest;
|
||||
}) {
|
||||
const {
|
||||
releaseLine = 'latest',
|
||||
packageInfoFetcher = fetchPackageInfo,
|
||||
releaseManifest,
|
||||
} = options;
|
||||
// The main release line is just an alias for latest
|
||||
const distTag = releaseLine === 'main' ? 'latest' : releaseLine;
|
||||
const found = new Map<string, string>();
|
||||
const releasePackages = new Map(
|
||||
releaseManifest?.packages.map(p => [p.name, p.version]),
|
||||
);
|
||||
return async function findTargetVersion(name: string) {
|
||||
const existing = found.get(name);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
console.log(`Checking for updates of ${name}`);
|
||||
const manifestVersion = releasePackages.get(name);
|
||||
if (manifestVersion) {
|
||||
return manifestVersion;
|
||||
}
|
||||
|
||||
const info = await packageInfoFetcher(name);
|
||||
const latestVersion = info['dist-tags'].latest;
|
||||
if (!latestVersion) {
|
||||
throw new Error(`No target 'latest' version found for ${name}`);
|
||||
}
|
||||
|
||||
const taggedVersion = info['dist-tags'][distTag];
|
||||
if (distTag === 'latest' || !taggedVersion) {
|
||||
found.set(name, latestVersion);
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
const latestVersionDateStr = info.time[latestVersion];
|
||||
const taggedVersionDateStr = info.time[taggedVersion];
|
||||
if (!latestVersionDateStr) {
|
||||
throw new Error(
|
||||
`No time available for version '${latestVersion}' of ${name}`,
|
||||
);
|
||||
}
|
||||
if (!taggedVersionDateStr) {
|
||||
throw new Error(
|
||||
`No time available for version '${taggedVersion}' of ${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const latestVersionRelease = new Date(latestVersionDateStr).getTime();
|
||||
const taggedVersionRelease = new Date(taggedVersionDateStr).getTime();
|
||||
if (latestVersionRelease > taggedVersionRelease) {
|
||||
// Prefer latest version if it's newer.
|
||||
found.set(name, latestVersion);
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
found.set(name, taggedVersion);
|
||||
return taggedVersion;
|
||||
};
|
||||
}
|
||||
|
||||
function getBackstageJsonPath() {
|
||||
return targetPaths.resolveRoot(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,
|
||||
useYarnPlugin?: boolean,
|
||||
) {
|
||||
const backstageJson = await getBackstageJson();
|
||||
const prevVersion = backstageJson?.version;
|
||||
|
||||
if (prevVersion === version) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { yellow, cyan, green } = chalk;
|
||||
if (prevVersion) {
|
||||
const from = encodeURIComponent(prevVersion);
|
||||
const to = encodeURIComponent(version);
|
||||
let link = `https://backstage.github.io/upgrade-helper/?from=${from}&to=${to}`;
|
||||
|
||||
if (useYarnPlugin) {
|
||||
link += '&yarnPlugin=1';
|
||||
}
|
||||
|
||||
console.log(
|
||||
yellow(
|
||||
`Upgraded from release ${green(prevVersion)} to ${green(
|
||||
version,
|
||||
)}, please review these template changes:`,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
console.log(` ${cyan(link)}`);
|
||||
console.log();
|
||||
} else {
|
||||
console.log(
|
||||
yellow(
|
||||
`Your project is now at version ${version}, which has been written to ${BACKSTAGE_JSON}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await fs.writeJson(
|
||||
getBackstageJsonPath(),
|
||||
{ ...backstageJson, version },
|
||||
{
|
||||
spaces: 2,
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function asLockfileVersion(version: string) {
|
||||
if (version === 'backstage:^') {
|
||||
return `backstage:${(await getBackstageJson())?.version}`;
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
MockDirectory,
|
||||
createMockDirectory,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import * as runObj from '@backstage/cli-common';
|
||||
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
|
||||
import migrate from './migrate';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
// Remove log coloring to simplify log matching
|
||||
jest.mock('chalk', () => ({
|
||||
red: (str: string) => str,
|
||||
blue: (str: string) => str,
|
||||
cyan: (str: string) => str,
|
||||
green: (str: string) => str,
|
||||
magenta: (str: string) => str,
|
||||
yellow: (str: string) => str,
|
||||
}));
|
||||
|
||||
let mockDir: MockDirectory;
|
||||
jest.mock('@backstage/cli-common', () => {
|
||||
const actual = jest.requireActual('@backstage/cli-common');
|
||||
return {
|
||||
...actual,
|
||||
findPaths: () => ({
|
||||
resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args),
|
||||
get targetDir() {
|
||||
return mockDir.path;
|
||||
},
|
||||
}),
|
||||
run: jest.fn().mockReturnValue({
|
||||
exitCode: null,
|
||||
waitForExit: jest.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
function expectLogsToMatch(receivedLogs: String[], expected: String[]): void {
|
||||
expect(receivedLogs.filter(Boolean).sort()).toEqual(expected.sort());
|
||||
}
|
||||
|
||||
describe('versions:migrate', () => {
|
||||
mockDir = createMockDirectory();
|
||||
beforeAll(() => overrideTargetPaths(mockDir.path));
|
||||
|
||||
beforeEach(() => {
|
||||
(runObj.run as jest.Mock).mockReturnValue({
|
||||
exitCode: null,
|
||||
waitForExit: jest.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(runObj.run as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
it('should bump to the moved version when the package is moved', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: ['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',
|
||||
},
|
||||
}),
|
||||
},
|
||||
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',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { warn, log: logs } = await withLogCollector(async () => {
|
||||
await migrate({ args: [], info: { usage: 'test', name: 'test' } });
|
||||
});
|
||||
|
||||
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)',
|
||||
'Found a moved package @backstage/custom-two@^1.0.0 -> @backstage-community/custom-two in b (dependencies)',
|
||||
]);
|
||||
|
||||
expectLogsToMatch(warn, [
|
||||
'Could not find package.json for @backstage/core@^1.0.5 in a (dependencies)',
|
||||
'Could not find package.json for @backstage/core@^1.0.3 in b (dependencies)',
|
||||
'Could not find package.json for @backstage/theme@^1.0.0 in b (dependencies)',
|
||||
]);
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
expect(runObj.run).toHaveBeenCalledWith(
|
||||
['yarn', 'install'],
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
const packageA = await fs.readJson(
|
||||
mockDir.resolve('packages/a/package.json'),
|
||||
);
|
||||
|
||||
expect(packageA).toEqual({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage-community/custom': '^1.0.1',
|
||||
'@backstage-community/custom-two': '^1.0.0',
|
||||
'@backstage/core': '^1.0.5',
|
||||
},
|
||||
});
|
||||
const packageB = await fs.readJson(
|
||||
mockDir.resolve('packages/b/package.json'),
|
||||
);
|
||||
expect(packageB).toEqual({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage-community/custom': '^1.1.0',
|
||||
'@backstage-community/custom-two': '^1.0.0',
|
||||
'@backstage/core': '^1.0.3',
|
||||
'@backstage/theme': '^1.0.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace the occurrences of the moved package in files inside the correct package', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: ['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',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await withLogCollector(async () => {
|
||||
await migrate({ args: [], info: { usage: 'test', name: 'test' } });
|
||||
});
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
expect(runObj.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 replace occurrences of changed packages, and is careful', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: ['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',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await withLogCollector(async () => {
|
||||
await migrate({ args: [], info: { usage: 'test', name: 'test' } });
|
||||
});
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
expect(runObj.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';",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node';
|
||||
import chalk from 'chalk';
|
||||
import { resolve as resolvePath, join as joinPath } from 'node:path';
|
||||
import { cli } from 'cleye';
|
||||
import { readJson, writeJson } from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { runYarnInstall } from '../../lib/utils';
|
||||
import replace from 'replace-in-file';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
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 ({ args, info }: CliCommandContext) => {
|
||||
const {
|
||||
flags: { pattern, skipCodeChanges },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
booleanFlagNegation: true,
|
||||
flags: {
|
||||
pattern: {
|
||||
type: String,
|
||||
description: 'Override glob for matching packages to upgrade',
|
||||
},
|
||||
skipCodeChanges: {
|
||||
type: Boolean,
|
||||
description: 'Skip code changes and only update package.json files',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const changed = await migrateMovedPackages({
|
||||
pattern,
|
||||
skipCodeChanges,
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
await runYarnInstall();
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
let didAnythingChange = false;
|
||||
|
||||
for (const pkg of packages) {
|
||||
const pkgName = pkg.packageJson.name;
|
||||
let didPackageChange = false;
|
||||
const movedPackages = new Map<string, string>();
|
||||
|
||||
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) {
|
||||
movedPackages.set(depName, 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,
|
||||
});
|
||||
|
||||
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`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return didAnythingChange;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createCliModule } from '@backstage/cli-node';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
export default createCliModule({
|
||||
packageJson,
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['versions:migrate'],
|
||||
description:
|
||||
'Migrate any plugins that have been moved to the @backstage-community namespace automatically',
|
||||
execute: { loader: () => import('./commands/versions/migrate') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['versions:bump'],
|
||||
description: 'Bump Backstage packages to the latest versions',
|
||||
execute: { loader: () => import('./commands/versions/bump') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-roles'],
|
||||
description: `Add package role field to packages that don't have it`,
|
||||
execute: {
|
||||
loader: () => import('./commands/packageRole'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-scripts'],
|
||||
description: 'Set package scripts according to each package role',
|
||||
execute: {
|
||||
loader: () => import('./commands/packageScripts'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-exports'],
|
||||
description: 'Synchronize package subpath export definitions',
|
||||
execute: {
|
||||
loader: () => import('./commands/packageExports'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-lint-configs'],
|
||||
description:
|
||||
'Migrates all packages to use @backstage/cli/config/eslint-factory',
|
||||
execute: {
|
||||
loader: () => import('./commands/packageLintConfigs'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'react-router-deps'],
|
||||
description:
|
||||
'Migrates the react-router dependencies for all packages to be peer dependencies',
|
||||
execute: {
|
||||
loader: () => import('./commands/reactRouterDeps'),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ora from 'ora';
|
||||
import chalk from 'chalk';
|
||||
import { run } from '@backstage/cli-common';
|
||||
|
||||
export async function runYarnInstall() {
|
||||
const spinner = ora({
|
||||
prefixText: `Running ${chalk.blue('yarn install')} to install new versions`,
|
||||
spinner: 'arc',
|
||||
color: 'green',
|
||||
}).start();
|
||||
|
||||
const installOutput = new Array<Buffer>();
|
||||
try {
|
||||
await run(['yarn', 'install'], {
|
||||
env: {
|
||||
FORCE_COLOR: 'true',
|
||||
// We filter out all of the npm_* environment variables that are added when
|
||||
// executing through yarn. This works around an issue where these variables
|
||||
// incorrectly override local yarn or npm config in the project directory.
|
||||
...Object.fromEntries(
|
||||
Object.entries(process.env).map(([name, value]) =>
|
||||
name.startsWith('npm_') ? [name, undefined] : [name, value],
|
||||
),
|
||||
),
|
||||
},
|
||||
onStdout: data => installOutput.push(data),
|
||||
onStderr: data => installOutput.push(data),
|
||||
}).waitForExit();
|
||||
spinner.succeed();
|
||||
} catch (error) {
|
||||
spinner.fail();
|
||||
process.stdout.write(Buffer.concat(installOutput));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as runObj from '@backstage/cli-common';
|
||||
import * as yarn from './yarn';
|
||||
import { fetchPackageInfo, mapDependencies } from './packages';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
jest.mock('@backstage/cli-common', () => {
|
||||
const actual = jest.requireActual('@backstage/cli-common');
|
||||
return {
|
||||
...actual,
|
||||
runOutput: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./yarn', () => {
|
||||
return {
|
||||
detectYarnVersion: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('fetchPackageInfo', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should forward info for yarn classic', async () => {
|
||||
jest
|
||||
.spyOn(runObj, 'runOutput')
|
||||
.mockResolvedValue(`{"type":"inspect","data":{"the":"data"}}`);
|
||||
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('classic');
|
||||
|
||||
await expect(fetchPackageInfo('my-package')).resolves.toEqual({
|
||||
the: 'data',
|
||||
});
|
||||
expect(runObj.runOutput).toHaveBeenCalledWith([
|
||||
'yarn',
|
||||
'info',
|
||||
'--json',
|
||||
'my-package',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should forward info for yarn berry', async () => {
|
||||
jest.spyOn(runObj, 'runOutput').mockResolvedValue(`{"the":"data"}`);
|
||||
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry');
|
||||
|
||||
await expect(fetchPackageInfo('my-package')).resolves.toEqual({
|
||||
the: 'data',
|
||||
});
|
||||
expect(runObj.runOutput).toHaveBeenCalledWith([
|
||||
'yarn',
|
||||
'npm',
|
||||
'info',
|
||||
'--json',
|
||||
'my-package',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw if no info with yarn classic', async () => {
|
||||
jest.spyOn(runObj, 'runOutput').mockResolvedValue('');
|
||||
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('classic');
|
||||
|
||||
await expect(fetchPackageInfo('my-package')).rejects.toThrow(
|
||||
new NotFoundError(`No package information found for package my-package`),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if no info with yarn berry', async () => {
|
||||
const error = new Error('Command failed');
|
||||
(error as Error & { stdout?: string }).stdout =
|
||||
'bla bla bla Response Code: 404 bla bla';
|
||||
jest.spyOn(runObj, 'runOutput').mockRejectedValue(error);
|
||||
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry');
|
||||
|
||||
await expect(fetchPackageInfo('my-package')).rejects.toThrow(
|
||||
new NotFoundError(`No package information found for package my-package`),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDependencies', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should read dependencies', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: ['pkgs/*'],
|
||||
}),
|
||||
pkgs: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '1 || 2',
|
||||
},
|
||||
}),
|
||||
},
|
||||
b: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage/core': '3',
|
||||
'@backstage/cli': '^0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dependencyMap = await mapDependencies(mockDir.path, '@backstage/*');
|
||||
expect(Array.from(dependencyMap)).toEqual([
|
||||
[
|
||||
'@backstage/core',
|
||||
[
|
||||
{
|
||||
name: 'a',
|
||||
range: '1 || 2',
|
||||
location: mockDir.resolve('pkgs/a'),
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
range: '3',
|
||||
location: mockDir.resolve('pkgs/b'),
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'@backstage/cli',
|
||||
[
|
||||
{
|
||||
name: 'b',
|
||||
range: '^0',
|
||||
location: mockDir.resolve('pkgs/b'),
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { minimatch } from 'minimatch';
|
||||
import { getPackages } from '@manypkg/get-packages';
|
||||
import { detectYarnVersion } from './yarn';
|
||||
import { runOutput } from '@backstage/cli-common';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
const DEP_TYPES = [
|
||||
'dependencies',
|
||||
'devDependencies',
|
||||
'peerDependencies',
|
||||
'optionalDependencies',
|
||||
] as const;
|
||||
|
||||
// Package data as returned by `yarn info`
|
||||
export type YarnInfoInspectData = {
|
||||
name: string;
|
||||
'dist-tags': Record<string, string>;
|
||||
versions: string[];
|
||||
time: { [version: string]: string };
|
||||
};
|
||||
|
||||
// Possible `yarn info` output
|
||||
type YarnInfo = {
|
||||
type: 'inspect';
|
||||
data: YarnInfoInspectData | { type: string; data: unknown };
|
||||
};
|
||||
|
||||
type PkgVersionInfo = {
|
||||
range: string;
|
||||
name: string;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export async function fetchPackageInfo(
|
||||
name: string,
|
||||
): Promise<YarnInfoInspectData> {
|
||||
const yarnVersion = await detectYarnVersion();
|
||||
|
||||
const cmd = yarnVersion === 'classic' ? ['info'] : ['npm', 'info'];
|
||||
try {
|
||||
const output = await runOutput(['yarn', ...cmd, '--json', name]);
|
||||
|
||||
if (!output) {
|
||||
throw new NotFoundError(
|
||||
`No package information found for package ${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (yarnVersion === 'berry') {
|
||||
return JSON.parse(output) as YarnInfoInspectData;
|
||||
}
|
||||
|
||||
const info = JSON.parse(output) as YarnInfo;
|
||||
if (info.type !== 'inspect') {
|
||||
throw new Error(`Received unknown yarn info for ${name}, ${output}`);
|
||||
}
|
||||
|
||||
return info.data as YarnInfoInspectData;
|
||||
} catch (error) {
|
||||
if (yarnVersion === 'classic') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (
|
||||
error instanceof Error &&
|
||||
'stdout' in error &&
|
||||
typeof error.stdout === 'string' &&
|
||||
error.stdout.includes('Response Code: 404')
|
||||
) {
|
||||
throw new NotFoundError(
|
||||
`No package information found for package ${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Map all dependencies in the repo as dependency => dependents */
|
||||
export async function mapDependencies(
|
||||
targetDir: string,
|
||||
pattern: string,
|
||||
): Promise<Map<string, PkgVersionInfo[]>> {
|
||||
const { packages, root } = await getPackages(targetDir);
|
||||
|
||||
// Include root package.json too
|
||||
packages.push(root);
|
||||
|
||||
const dependencyMap = new Map<string, PkgVersionInfo[]>();
|
||||
for (const pkg of packages) {
|
||||
const deps = DEP_TYPES.flatMap(
|
||||
t => Object.entries(pkg.packageJson[t] ?? {}) as [string, string][],
|
||||
);
|
||||
|
||||
for (const [name, range] of deps) {
|
||||
if (minimatch(name, pattern)) {
|
||||
dependencyMap.set(
|
||||
name,
|
||||
(dependencyMap.get(name) ?? []).concat({
|
||||
range,
|
||||
name: pkg.packageJson.name,
|
||||
location: pkg.dir,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dependencyMap;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { assertError, ForwardedError } from '@backstage/errors';
|
||||
import { runOutput } from '@backstage/cli-common';
|
||||
|
||||
const versions = new Map<string, Promise<'classic' | 'berry'>>();
|
||||
|
||||
export function detectYarnVersion(dir?: string): Promise<'classic' | 'berry'> {
|
||||
const cwd = dir ?? process.cwd();
|
||||
if (versions.has(cwd)) {
|
||||
return versions.get(cwd)!;
|
||||
}
|
||||
|
||||
const promise = Promise.resolve().then(async () => {
|
||||
try {
|
||||
const stdout = await runOutput(['yarn', '--version'], {
|
||||
cwd,
|
||||
});
|
||||
return stdout.trim().startsWith('1.') ? 'classic' : 'berry';
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
throw new ForwardedError('Failed to determine yarn version', error);
|
||||
}
|
||||
});
|
||||
|
||||
versions.set(cwd, promise);
|
||||
return promise;
|
||||
}
|
||||
Reference in New Issue
Block a user