Merge pull request #27938 from backstage/yarn-plugin-http-utils
yarn-plugin: use yarn's httpUtils to fetch release manifests
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'yarn-plugin-backstage': patch
|
||||
---
|
||||
|
||||
Use yarn's built-in http utilities for fetching release manifests
|
||||
@@ -35,7 +35,6 @@
|
||||
"@yarnpkg/core": "^4.0.3",
|
||||
"@yarnpkg/fslib": "^3.0.2",
|
||||
"@yarnpkg/plugin-pack": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"semver": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -14,35 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Manifest, Workspace } from '@yarnpkg/core';
|
||||
import {
|
||||
Configuration,
|
||||
Manifest,
|
||||
Project,
|
||||
Workspace,
|
||||
httpUtils,
|
||||
} from '@yarnpkg/core';
|
||||
import { npath, ppath } from '@yarnpkg/fslib';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
import { beforeWorkspacePacking } from './beforeWorkspacePacking';
|
||||
|
||||
jest.mock('@backstage/release-manifests', () => ({
|
||||
getManifestByVersion: jest.fn().mockResolvedValue({
|
||||
releaseVersion: '1.23.45',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '3.2.1',
|
||||
},
|
||||
{
|
||||
name: '@backstage/plugin-1',
|
||||
version: '6.5.4',
|
||||
},
|
||||
{
|
||||
name: '@backstage/plugin-2',
|
||||
version: '9.8.7',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
const makeWorkspace = (manifest: object) => {
|
||||
return {
|
||||
manifest: Manifest.fromText(JSON.stringify(manifest)),
|
||||
project: new Project(ppath.cwd(), {
|
||||
configuration: Configuration.create(ppath.cwd()),
|
||||
}),
|
||||
} as Workspace;
|
||||
};
|
||||
|
||||
@@ -58,6 +47,24 @@ describe('beforeWorkspacePacking', () => {
|
||||
.spyOn(process, 'cwd')
|
||||
.mockReturnValue(npath.toPortablePath(mockDir.path));
|
||||
|
||||
jest.spyOn(httpUtils, 'get').mockResolvedValue({
|
||||
releaseVersion: '1.23.45',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '3.2.1',
|
||||
},
|
||||
{
|
||||
name: '@backstage/plugin-1',
|
||||
version: '6.5.4',
|
||||
},
|
||||
{
|
||||
name: '@backstage/plugin-2',
|
||||
version: '9.8.7',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockDir.setContent({
|
||||
'backstage.json': JSON.stringify({
|
||||
version: '1.23.45',
|
||||
|
||||
@@ -70,6 +70,7 @@ export const beforeWorkspacePacking = async (
|
||||
|
||||
rawManifest[finalDependencyType][ident] = `^${await getPackageVersion(
|
||||
bindBackstageVersion(descriptor, backstageVersion),
|
||||
workspace.project.configuration,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,31 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { structUtils } from '@yarnpkg/core';
|
||||
import {
|
||||
Configuration,
|
||||
Project,
|
||||
ResolveOptions,
|
||||
structUtils,
|
||||
ThrowReport,
|
||||
httpUtils,
|
||||
} from '@yarnpkg/core';
|
||||
import { npath, ppath } from '@yarnpkg/fslib';
|
||||
import { getManifestByVersion } from '@backstage/release-manifests';
|
||||
import { BackstageResolver } from './BackstageResolver';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
jest.mock('@backstage/release-manifests', () => ({
|
||||
getManifestByVersion: jest.fn().mockResolvedValue({
|
||||
releaseVersion: '1.23.45',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '6.7.8',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
const getManifestByVersionMock = getManifestByVersion as jest.MockedFunction<
|
||||
typeof getManifestByVersion
|
||||
>;
|
||||
|
||||
describe('BackstageResolver', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
let backstageResolver: BackstageResolver;
|
||||
let resolveOptions: ResolveOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
@@ -49,6 +40,16 @@ describe('BackstageResolver', () => {
|
||||
.spyOn(process, 'cwd')
|
||||
.mockReturnValue(npath.toPortablePath(mockDir.path));
|
||||
|
||||
jest.spyOn(httpUtils, 'get').mockResolvedValue({
|
||||
releaseVersion: '1.23.45',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '6.7.8',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockDir.setContent({
|
||||
'backstage.json': JSON.stringify({
|
||||
version: '1.23.45',
|
||||
@@ -71,6 +72,14 @@ describe('BackstageResolver', () => {
|
||||
});
|
||||
|
||||
backstageResolver = new BackstageResolver();
|
||||
|
||||
resolveOptions = {
|
||||
resolver: backstageResolver,
|
||||
project: new Project(ppath.cwd(), {
|
||||
configuration: Configuration.create(ppath.cwd()),
|
||||
}),
|
||||
report: new ThrowReport(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -140,7 +149,7 @@ describe('BackstageResolver', () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
backstageResolver.getCandidates(descriptor),
|
||||
backstageResolver.getCandidates(descriptor, {}, resolveOptions),
|
||||
).resolves.toEqual([structUtils.makeLocator(descriptor, 'npm:6.7.8')]);
|
||||
});
|
||||
|
||||
@@ -151,6 +160,8 @@ describe('BackstageResolver', () => {
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'npm:1.2.3',
|
||||
),
|
||||
{},
|
||||
resolveOptions,
|
||||
),
|
||||
).rejects.toThrow(/unsupported version protocol/i);
|
||||
});
|
||||
@@ -162,6 +173,8 @@ describe('BackstageResolver', () => {
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:^',
|
||||
),
|
||||
{},
|
||||
resolveOptions,
|
||||
),
|
||||
).rejects.toThrow(/missing Backstage version/i);
|
||||
});
|
||||
@@ -173,6 +186,8 @@ describe('BackstageResolver', () => {
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:^::v=1&v=2',
|
||||
),
|
||||
{},
|
||||
resolveOptions,
|
||||
),
|
||||
).rejects.toThrow(/multiple Backstage versions/i);
|
||||
});
|
||||
@@ -190,34 +205,12 @@ describe('BackstageResolver', () => {
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
`backstage:${selector}`,
|
||||
),
|
||||
{},
|
||||
resolveOptions,
|
||||
),
|
||||
).rejects.toThrow(/unexpected version selector/i);
|
||||
},
|
||||
);
|
||||
|
||||
it('memoizes manifest retrieval', async () => {
|
||||
const descriptor1 = structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:^::v=1.23.45',
|
||||
);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await backstageResolver.getCandidates(descriptor1);
|
||||
}
|
||||
|
||||
expect(getManifestByVersionMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
const descriptor2 = structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:^::v=6.78.90',
|
||||
);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await backstageResolver.getCandidates(descriptor2);
|
||||
}
|
||||
|
||||
expect(getManifestByVersionMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSatisfying', () => {
|
||||
@@ -243,6 +236,7 @@ describe('BackstageResolver', () => {
|
||||
'npm:1.2.3',
|
||||
),
|
||||
],
|
||||
resolveOptions,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
locators: [
|
||||
@@ -277,6 +271,7 @@ describe('BackstageResolver', () => {
|
||||
'npm:7.8.9',
|
||||
),
|
||||
],
|
||||
resolveOptions,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
locators: [
|
||||
@@ -298,6 +293,7 @@ describe('BackstageResolver', () => {
|
||||
),
|
||||
{},
|
||||
[],
|
||||
resolveOptions,
|
||||
),
|
||||
).rejects.toThrow(/unsupported version protocol/i);
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Locator,
|
||||
Package,
|
||||
Resolver,
|
||||
ResolveOptions,
|
||||
} from '@yarnpkg/core';
|
||||
import { PROTOCOL } from '../constants';
|
||||
import {
|
||||
@@ -60,11 +61,18 @@ export class BackstageResolver implements Resolver {
|
||||
* concrete version into the appropriate concrete npm version for that
|
||||
* backstage release.
|
||||
*/
|
||||
async getCandidates(descriptor: Descriptor): Promise<Locator[]> {
|
||||
async getCandidates(
|
||||
descriptor: Descriptor,
|
||||
_dependencies: Record<string, Package>,
|
||||
opts: ResolveOptions,
|
||||
): Promise<Locator[]> {
|
||||
return [
|
||||
structUtils.makeLocator(
|
||||
descriptor,
|
||||
`npm:${await getPackageVersion(descriptor)}`,
|
||||
`npm:${await getPackageVersion(
|
||||
descriptor,
|
||||
opts.project.configuration,
|
||||
)}`,
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -79,8 +87,12 @@ export class BackstageResolver implements Resolver {
|
||||
descriptor: Descriptor,
|
||||
_dependencies: Record<string, Package>,
|
||||
locators: Array<Locator>,
|
||||
opts: ResolveOptions,
|
||||
): Promise<{ locators: Locator[]; sorted: boolean }> {
|
||||
const packageVersion = await getPackageVersion(descriptor);
|
||||
const packageVersion = await getPackageVersion(
|
||||
descriptor,
|
||||
opts.project.configuration,
|
||||
);
|
||||
|
||||
return {
|
||||
locators: locators.filter(
|
||||
|
||||
@@ -14,18 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Configuration,
|
||||
Descriptor,
|
||||
httpUtils,
|
||||
structUtils,
|
||||
} from '@yarnpkg/core';
|
||||
import { ppath, xfs } from '@yarnpkg/fslib';
|
||||
import { valid as semverValid } from 'semver';
|
||||
import memoize from 'lodash/memoize';
|
||||
import { getManifestByVersion as getManifestByVersionBase } from '@backstage/release-manifests';
|
||||
import { BACKSTAGE_JSON, findPaths } from '@backstage/cli-common';
|
||||
import { Descriptor, structUtils } from '@yarnpkg/core';
|
||||
import { PROTOCOL } from './constants';
|
||||
import { getManifestByVersion } from '@backstage/release-manifests';
|
||||
|
||||
const getManifestByVersion = memoize(
|
||||
getManifestByVersionBase,
|
||||
({ version }) => version,
|
||||
);
|
||||
import { PROTOCOL } from './constants';
|
||||
|
||||
export const getCurrentBackstageVersion = () => {
|
||||
const workspaceRoot = ppath.resolve(findPaths(ppath.cwd()).targetRoot);
|
||||
@@ -50,7 +50,10 @@ export const bindBackstageVersion = (
|
||||
return structUtils.bindDescriptor(descriptor, { v: backstageVersion });
|
||||
};
|
||||
|
||||
export const getPackageVersion = async (descriptor: Descriptor) => {
|
||||
export const getPackageVersion = async (
|
||||
descriptor: Descriptor,
|
||||
configuration: Configuration,
|
||||
) => {
|
||||
const ident = structUtils.stringifyIdent(descriptor);
|
||||
const range = structUtils.parseRange(descriptor.range);
|
||||
|
||||
@@ -80,6 +83,36 @@ export const getPackageVersion = async (descriptor: Descriptor) => {
|
||||
|
||||
const manifest = await getManifestByVersion({
|
||||
version: range.params.v,
|
||||
// We override the fetch function used inside getManifestByVersion with a
|
||||
// custom implementation that calls yarn's built-in `httpUtils` method
|
||||
// instead. This has a couple of benefits:
|
||||
//
|
||||
// 1. This means that the fetch should leverage yarn's built-in cache, so we
|
||||
// don't need to explicitly memoize the fetch.
|
||||
// 2. The request should automatically take account of any proxy settings
|
||||
// configured in yarn.
|
||||
fetch: async (url: string) => {
|
||||
const response = await httpUtils.get(url, {
|
||||
configuration,
|
||||
jsonResponse: true,
|
||||
});
|
||||
|
||||
// The release-manifests package expects fetchFn to resolve with a subset
|
||||
// of the native HTTP Response object, but yarn's httpUtils implementation
|
||||
// keeps most of the details hidden. This means we need to construct an
|
||||
// object which quacks like a Response in the appropriate ways.
|
||||
return {
|
||||
// The function has some custom handling for non-200 errors. Yarn
|
||||
// doesn't provide the status code, but if we've got to this point
|
||||
// without throwing, we can assume the request has been successful.
|
||||
status: 200,
|
||||
// The requested URL, used to correctly report errors
|
||||
url,
|
||||
// Yarn automatically parses the response as JSON, so our implementation
|
||||
// can simply return it.
|
||||
json: () => response,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const manifestEntry = manifest.packages.find(
|
||||
|
||||
Reference in New Issue
Block a user