cli: refactor out parts of versioning:bump + add tests

This commit is contained in:
Patrik Oldsberg
2020-11-22 18:17:55 +01:00
parent 9913459698
commit c0f7591dd7
4 changed files with 209 additions and 49 deletions
+9 -49
View File
@@ -17,11 +17,13 @@
import fs from 'fs-extra';
import semver from 'semver';
import { resolve as resolvePath } from 'path';
import { run, runPlain } from '../../lib/run';
import { run } from '../../lib/run';
import { paths } from '../../lib/paths';
import { Lockfile } from '../../lib/versioning';
const PREFIX = '@backstage';
import {
mapDependencies,
fetchPackageInfo,
Lockfile,
} from '../../lib/versioning';
const DEP_TYPES = [
'dependencies',
@@ -30,20 +32,6 @@ const DEP_TYPES = [
'optionalDependencies',
];
// Package data as returned by `yarn info`
type YarnInfoInspectData = {
name: string;
'dist-tags': { latest: 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;
@@ -51,43 +39,15 @@ type PkgVersionInfo = {
};
export default async () => {
const LernaProject = require('@lerna/project');
const project = new LernaProject(paths.targetDir);
const packages = await project.getPackages();
// First we discover all Backstage dependencies within our own repo
const dependencyMap = new Map<string, PkgVersionInfo[]>();
for (const pkg of packages) {
const deps = DEP_TYPES.flatMap(
t => Object.entries(pkg.get(t) ?? {}) as [string, string][],
);
for (const [name, range] of deps) {
if (name.startsWith(PREFIX)) {
dependencyMap.set(
name,
(dependencyMap.get(name) ?? []).concat({
range,
name: pkg.name,
location: pkg.location,
}),
);
}
}
}
const dependencyMap = await mapDependencies();
// Next check with the package registry what the latest version of all of those dependencies are
const targetVersions = new Map<string, string>();
await workerThreads(16, dependencyMap.keys(), async name => {
console.log(`Checking for updates of ${name}`);
const output = await runPlain('yarn', 'info', '--json', name);
const info = JSON.parse(output) as YarnInfo;
if (info.type !== 'inspect') {
throw new Error(`Received unknown yarn info for ${name}, ${output}`);
}
const data = info.data as YarnInfoInspectData;
const latest = data['dist-tags'].latest;
const info = await fetchPackageInfo(name);
const latest = info['dist-tags'].latest;
if (!latest) {
throw new Error(`No latest version found for ${name}`);
}
+1
View File
@@ -15,3 +15,4 @@
*/
export { Lockfile } from './Lockfile';
export { fetchPackageInfo, mapDependencies } from './packages';
@@ -0,0 +1,109 @@
/*
* Copyright 2020 Spotify AB
*
* 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 mockFs from 'mock-fs';
import * as runObj from '../run';
import { paths } from '../paths';
import { fetchPackageInfo, mapDependencies } from './packages';
describe('fetchPackageInfo', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should forward info', async () => {
jest
.spyOn(runObj, 'runPlain')
.mockResolvedValue(`{"type":"inspect","data":{"the":"data"}}`);
await expect(fetchPackageInfo('my-package')).resolves.toEqual({
the: 'data',
});
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'my-package',
);
});
});
describe('mapDependencies', () => {
afterEach(() => {
mockFs.restore();
jest.resetAllMocks();
});
it('should derp', async () => {
// Make sure all modules involved in package discovery are in the module cache before we mock fs
const LernaProject = require('@lerna/project');
const project = new LernaProject(paths.targetDir);
await project.getPackages();
mockFs({
'/lerna.json': JSON.stringify({
packages: ['pkgs/*'],
}),
'/pkgs/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': '1 || 2',
},
}),
'/pkgs/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/core': '3',
'@backstage/cli': '^0',
},
}),
});
const oldDir = paths.targetDir;
paths.targetDir = '/';
const dependencyMap = await mapDependencies();
expect(Array.from(dependencyMap)).toEqual([
[
'@backstage/core',
[
{
name: 'a',
range: '1 || 2',
location: '/pkgs/a',
},
{
name: 'b',
range: '3',
location: '/pkgs/b',
},
],
],
[
'@backstage/cli',
[
{
name: 'b',
range: '^0',
location: '/pkgs/b',
},
],
],
]);
paths.targetDir = oldDir;
});
});
@@ -0,0 +1,90 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { runPlain } from '../../lib/run';
import { paths } from '../../lib/paths';
const PREFIX = '@backstage';
const DEP_TYPES = [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
];
// Package data as returned by `yarn info`
type YarnInfoInspectData = {
name: string;
'dist-tags': { latest: 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 output = await runPlain('yarn', 'info', '--json', name);
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;
}
/** Map all dependencies in the repo as dependency => dependents */
export async function mapDependencies(): Promise<
Map<string, PkgVersionInfo[]>
> {
const LernaProject = require('@lerna/project');
const project = new LernaProject(paths.targetDir);
const packages = await project.getPackages();
const dependencyMap = new Map<string, PkgVersionInfo[]>();
for (const pkg of packages) {
const deps = DEP_TYPES.flatMap(
t => Object.entries(pkg.get(t) ?? {}) as [string, string][],
);
for (const [name, range] of deps) {
if (name.startsWith(PREFIX)) {
dependencyMap.set(
name,
(dependencyMap.get(name) ?? []).concat({
range,
name: pkg.name,
location: pkg.location,
}),
);
}
}
}
return dependencyMap;
}