cli: update versions:bump to work with Yarn 3
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Updated `versions:bump` command to be compatible with Yarn 3.
|
||||
@@ -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(
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user