Merge branch 'master' of https://github.com/backstage/backstage into feat/allow-inspect-override

Signed-off-by: jrwpatterson <jrwpatterson@gmail.com>
This commit is contained in:
jrwpatterson
2023-09-13 11:09:25 +10:00
632 changed files with 11955 additions and 1558 deletions
-142
View File
@@ -1,142 +0,0 @@
/*
* 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 { paths } from '../lib/paths';
import { ESLint } from 'eslint';
import { join as joinPath, basename } from 'path';
import fs from 'fs-extra';
import { isChildPath } from '@backstage/cli-common';
import { PackageGraph } from '@backstage/cli-node';
function isTestPath(filePath: string) {
if (!isChildPath(joinPath(paths.targetDir, 'src'), filePath)) {
return true;
}
const name = basename(filePath);
return (
name.startsWith('setupTests.') ||
name.includes('.test.') ||
name.includes('.stories.')
);
}
export async function command() {
const pkgJsonPath = paths.resolveTarget('package.json');
const pkg = await fs.readJson(pkgJsonPath);
if (pkg.workspaces) {
throw new Error(
'Adding dependencies to the workspace root is not supported',
);
}
const packages = await PackageGraph.listTargetPackages();
const localPackageVersions = new Map(
packages.map(p => [p.packageJson.name, p.packageJson.version]),
);
const eslint = new ESLint({
cwd: paths.targetDir,
overrideConfig: {
plugins: ['monorepo'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
`!${joinPath(paths.targetDir, 'src/**')}`,
joinPath(paths.targetDir, 'src/**/*.test.*'),
joinPath(paths.targetDir, 'src/**/*.stories.*'),
joinPath(paths.targetDir, 'src/setupTests.*'),
],
optionalDependencies: true,
peerDependencies: true,
bundledDependencies: true,
},
],
},
},
extensions: ['jsx', 'ts', 'tsx', 'mjs', 'cjs'],
});
const results = await eslint.lintFiles(['.']);
const addedDeps = new Set<string>();
const addedDevDeps = new Set<string>();
const removedDevDeps = new Set<string>();
for (const result of results) {
for (const message of result.messages) {
// Just in case
if (message.ruleId !== 'import/no-extraneous-dependencies') {
continue;
}
const match = message.message.match(/^'([^']*)' should be listed/);
if (!match) {
continue;
}
const packageName = match[1];
if (!localPackageVersions.has(packageName)) {
continue;
}
if (message.message.endsWith('not devDependencies.')) {
addedDeps.add(packageName);
removedDevDeps.add(packageName);
} else if (isTestPath(result.filePath)) {
addedDevDeps.add(packageName);
} else {
addedDeps.add(packageName);
}
}
}
if (addedDeps.size || addedDevDeps.size || removedDevDeps.size) {
for (const name of addedDeps) {
if (!pkg.dependencies) {
pkg.dependencies = {};
}
pkg.dependencies[name] = `^${localPackageVersions.get(name)}`;
}
for (const name of addedDevDeps) {
if (!pkg.devDependencies) {
pkg.devDependencies = {};
}
pkg.devDependencies[name] = `^${localPackageVersions.get(name)}`;
}
for (const name of removedDevDeps) {
delete pkg.devDependencies[name];
}
if (Object.keys(pkg.devDependencies).length === 0) {
delete pkg.devDependencies;
}
if (pkg.dependencies) {
pkg.dependencies = Object.fromEntries(
Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b)),
);
}
if (pkg.devDependencies) {
pkg.devDependencies = Object.fromEntries(
Object.entries(pkg.devDependencies).sort(([a], [b]) =>
a.localeCompare(b),
),
);
}
await fs.writeJson(pkgJsonPath, pkg, { spaces: 2 });
}
}
-6
View File
@@ -168,12 +168,6 @@ export function registerScriptCommand(program: Command) {
.description('Run tests, forwarding args to Jest, defaulting to watch mode')
.action(lazy(() => import('./test').then(m => m.default)));
command
.command('fix', { hidden: true })
.description('Applies automated fixes to the package. [EXPERIMENTAL]')
.option('--deps', 'Only fix monorepo dependencies in package.json')
.action(lazy(() => import('./fix').then(m => m.command)));
command
.command('clean')
.description('Delete cache directories')
+100 -40
View File
@@ -29,6 +29,7 @@ import { YarnInfoInspectData } from '../../lib/versioning/packages';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { NotFoundError } from '@backstage/errors';
import { Lockfile } from '../../lib/versioning/Lockfile';
// Remove log coloring to simplify log matching
jest.mock('chalk', () => ({
@@ -224,7 +225,7 @@ describe('bump', () => {
});
});
it('should bump backstage dependencies but not them installed', async () => {
it('should bump backstage dependencies but not install them', async () => {
mockFs({
'/yarn.lock': lockfileMock,
'/package.json': JSON.stringify({
@@ -453,68 +454,36 @@ describe('bump', () => {
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
'https://versions.backstage.io/v1/releases/1.0.0/manifest.json',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
releaseVersion: '2.0.0',
packages: [
{ name: '@backstage/core', version: '5.0.0' },
{ name: '@backstage/core-api', version: '5.0.0' },
],
}),
),
'https://versions.backstage.io/v1/releases/999.0.1/manifest.json',
(_, res, ctx) => res(ctx.status(404), ctx.json({})),
),
);
const { log: logs } = await withLogCollector(['log'], async () => {
await expect(
bump({ pattern: null, release: '1.0.0' } as unknown as Command),
).rejects.toThrow('Duplicate versions present after package bump');
bump({ pattern: null, release: '999.0.1' } as unknown as Command),
).rejects.toThrow('No release found for 999.0.1 version');
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Package info not found, ignoring package @backstage/theme',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core-api',
'Package info not found, ignoring package @backstage/theme',
'Some packages are outdated, updating',
'bumping @backstage/core in a to ^5.0.0',
'bumping @backstage/core in b to ^5.0.0',
'Your project is now at version 2.0.0, which has been written to backstage.json',
'Running yarn install to install new versions',
'⚠️ The following packages may have breaking changes:',
' @backstage/core : 1.0.3 ~> 5.0.0',
' https://github.com/backstage/backstage/blob/master/packages/core/CHANGELOG.md',
'Version bump complete!',
]);
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
'yarn',
['install'],
expect.any(Object),
);
expect(runObj.run).toHaveBeenCalledTimes(0);
const packageA = await fs.readJson('/packages/a/package.json');
expect(packageA).toEqual({
name: 'a',
dependencies: {
'@backstage/core': '^5.0.0',
'@backstage/core': '^1.0.5',
},
});
const packageB = await fs.readJson('/packages/b/package.json');
expect(packageB).toEqual({
name: 'b',
dependencies: {
'@backstage/core': '^5.0.0',
'@backstage/core': '^1.0.3',
'@backstage/theme': '^1.0.0',
},
});
expect(await fs.readJson('/backstage.json')).toEqual({ version: '2.0.0' });
});
it('should prefer versions from the highest manifest version when main is not specified', async () => {
@@ -825,6 +794,97 @@ describe('bump', () => {
},
});
});
it('should log duplicates', async () => {
jest.spyOn(Lockfile.prototype, 'analyze').mockReturnValue({
invalidRanges: [],
newVersions: [],
newRanges: [
{
name: 'first-duplicate',
oldRange: 'first-duplicate',
newRange: 'first-duplicate',
oldVersion: '1.0.0',
newVersion: '2.0.0',
},
{
name: 'second-duplicate',
oldRange: 'second-duplicate',
newRange: 'second-duplicate',
oldVersion: '1.0.0',
newVersion: '2.0.0',
},
{
name: 'third-duplicate',
oldRange: 'third-duplicate',
newRange: 'third-duplicate',
oldVersion: '1.0.0',
newVersion: '2.0.0',
},
],
});
mockFs({
'/yarn.lock': lockfileMock,
'/package.json': JSON.stringify({
workspaces: {
packages: ['packages/*'],
},
}),
'/packages/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': '^1.0.5',
},
}),
'/packages/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/core': '^1.0.3',
'@backstage/theme': '^1.0.0',
},
}),
});
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
packages: [],
}),
),
),
);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump({ pattern: null, release: 'main' } as unknown as Command);
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
'bumping @backstage/core in a to ^1.0.6',
'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',
'⚠️ 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',
'Version bump complete!',
'The following packages have duplicates but have been allowed:',
'first-duplicate, second-duplicate, third-duplicate',
]);
});
});
describe('bumpBackstageJsonVersion', () => {
+13 -4
View File
@@ -314,10 +314,6 @@ export default async (opts: OptionValues) => {
),
});
if (result.newVersions.length > 0) {
throw new Error('Duplicate versions present after package bump');
}
const forbiddenNewRanges = result.newRanges.filter(({ name }) =>
forbiddenDuplicatesFilter(name),
);
@@ -328,6 +324,19 @@ export default async (opts: OptionValues) => {
.join(', ')}`,
);
}
const allowedDuplicates = result.newRanges.filter(
({ name }) => !forbiddenDuplicatesFilter(name),
);
if (allowedDuplicates.length > 0) {
console.log(
chalk.yellow(
'The following packages have duplicates but have been allowed:',
),
);
console.log(chalk.yellow(allowedDuplicates.map(i => i.name).join(', ')));
}
};
export function createStrictVersionFinder(options: {