cli: update versions:bump to work with Yarn 3

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-09-08 13:01:46 +02:00
parent ce7aac7751
commit a7e82c9b01
6 changed files with 173 additions and 119 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated `versions:bump` command to be compatible with Yarn 3.
+29 -96
View File
@@ -28,6 +28,7 @@ import {
import { YarnInfoInspectData } from '../../lib/versioning/packages';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { NotFoundError } from '@backstage/errors';
// Remove log coloring to simplify log matching
jest.mock('chalk', () => ({
@@ -54,7 +55,15 @@ jest.mock('ora', () => ({
jest.mock('../../lib/run', () => {
return {
run: jest.fn(),
runPlain: jest.fn(),
};
});
const mockFetchPackageInfo = jest.fn();
jest.mock('../../lib/versioning/packages', () => {
const actual = jest.requireActual('../../lib/versioning/packages');
return {
...actual,
fetchPackageInfo: (name: string) => mockFetchPackageInfo(name),
};
});
@@ -105,6 +114,14 @@ const lockfileMockResult = `${HEADER}
`;
describe('bump', () => {
beforeEach(() => {
mockFetchPackageInfo.mockImplementation(async name => ({
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
}));
});
afterEach(() => {
mockFs.restore();
jest.resetAllMocks();
@@ -138,17 +155,6 @@ describe('bump', () => {
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
},
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
@@ -184,19 +190,10 @@ describe('bump', () => {
'Version bump complete!',
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(3);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/core',
);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/theme',
);
expect(mockFetchPackageInfo).toHaveBeenCalledTimes(3);
expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core');
expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core-api');
expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/theme');
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
@@ -251,17 +248,6 @@ describe('bump', () => {
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
},
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
@@ -309,19 +295,9 @@ describe('bump', () => {
'Version bump complete!',
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(2);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/core',
);
expect(runObj.runPlain).not.toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/theme',
);
expect(mockFetchPackageInfo).toHaveBeenCalledTimes(2);
expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core');
expect(mockFetchPackageInfo).not.toHaveBeenCalledWith('@backstage/theme');
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
@@ -372,17 +348,6 @@ describe('bump', () => {
},
}),
});
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
},
}),
);
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
@@ -480,17 +445,6 @@ describe('bump', () => {
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
},
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
@@ -614,17 +568,6 @@ describe('bump', () => {
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
},
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
@@ -672,19 +615,9 @@ describe('bump', () => {
'Version bump complete!',
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(5);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/core',
);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/theme',
);
expect(mockFetchPackageInfo).toHaveBeenCalledTimes(5);
expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core');
expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/theme');
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
@@ -743,7 +676,7 @@ describe('bump', () => {
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async () => '');
mockFetchPackageInfo.mockRejectedValue(new NotFoundError('Nope'));
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
+1 -1
View File
@@ -25,7 +25,7 @@ import { promisify } from 'util';
import { LogFunc } from './logging';
import { assertError, ForwardedError } from '@backstage/errors';
const execFile = promisify(execFileCb);
export const execFile = promisify(execFileCb);
type SpawnOptionsPartialEnv = Omit<SpawnOptions, 'env'> & {
env?: Partial<NodeJS.ProcessEnv>;
@@ -17,13 +17,20 @@
import mockFs from 'mock-fs';
import path from 'path';
import * as runObj from '../run';
import * as yarn from '../yarn';
import { fetchPackageInfo, mapDependencies } from './packages';
import { NotFoundError } from '../errors';
jest.mock('../run', () => {
return {
run: jest.fn(),
runPlain: jest.fn(),
execFile: jest.fn(),
};
});
jest.mock('../yarn', () => {
return {
detectYarnVersion: jest.fn(),
};
});
@@ -32,24 +39,55 @@ describe('fetchPackageInfo', () => {
jest.resetAllMocks();
});
it('should forward info', async () => {
jest
.spyOn(runObj, 'runPlain')
.mockResolvedValue(`{"type":"inspect","data":{"the":"data"}}`);
it('should forward info for yarn classic', async () => {
jest.spyOn(runObj, 'execFile').mockResolvedValue({
stdout: `{"type":"inspect","data":{"the":"data"}}`,
stderr: '',
});
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('classic');
await expect(fetchPackageInfo('my-package')).resolves.toEqual({
the: 'data',
});
expect(runObj.runPlain).toHaveBeenCalledWith(
expect(runObj.execFile).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'my-package',
['info', '--json', 'my-package'],
{ shell: true },
);
});
it('should throw if no info', async () => {
jest.spyOn(runObj, 'runPlain').mockResolvedValue('');
it('should forward info for yarn berry', async () => {
jest
.spyOn(runObj, 'execFile')
.mockResolvedValue({ stdout: `{"the":"data"}`, stderr: '' });
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry');
await expect(fetchPackageInfo('my-package')).resolves.toEqual({
the: 'data',
});
expect(runObj.execFile).toHaveBeenCalledWith(
'yarn',
['npm', 'info', '--json', 'my-package'],
{ shell: true },
);
});
it('should throw if no info with yarn classic', async () => {
jest
.spyOn(runObj, 'execFile')
.mockResolvedValue({ stdout: '', stderr: '' });
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 () => {
jest
.spyOn(runObj, 'execFile')
.mockRejectedValue({ stdout: 'bla bla bla Response Code: 404 bla bla' });
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry');
await expect(fetchPackageInfo('my-package')).rejects.toThrow(
new NotFoundError(`No package information found for package my-package`),
+40 -11
View File
@@ -13,10 +13,12 @@
* 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 { runPlain } from '../../lib/run';
import { NotFoundError } from '../errors';
import { detectYarnVersion } from '../yarn';
import { execFile } from '../run';
const DEP_TYPES = [
'dependencies',
@@ -48,18 +50,45 @@ type PkgVersionInfo = {
export async function fetchPackageInfo(
name: string,
): Promise<YarnInfoInspectData> {
const output = await runPlain('yarn', 'info', '--json', name);
const yarnVersion = await detectYarnVersion();
if (!output) {
throw new NotFoundError(`No package information found for package ${name}`);
const cmd = yarnVersion === 'classic' ? ['info'] : ['npm', 'info'];
try {
const { stdout: output } = await execFile(
'yarn',
[...cmd, '--json', name],
{ shell: true },
);
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?.stdout.includes('Response Code: 404')) {
throw new NotFoundError(
`No package information found for package ${name}`,
);
}
throw error;
}
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 */
+49
View File
@@ -0,0 +1,49 @@
/*
* 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 { execFile as execFileCb } from 'child_process';
import { promisify } from 'util';
const execFile = promisify(execFileCb);
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 execFile('yarn', ['--version'], {
shell: true,
cwd,
});
return stdout.trim().startsWith('1.') ? 'classic' : 'berry';
} catch (error) {
assertError(error);
if ('stderr' in error) {
process.stderr.write(error.stderr as Buffer);
}
throw new ForwardedError('Failed to determine yarn version', error);
}
});
versions.set(cwd, promise);
return promise;
}