yarn-plugin: use yarn's httpUtils to fetch release manifests

This allows us to leverage yarn's built in proxy
configuration when making the request. The yarn
HTTP utilities handle caching of GET
requests, so we can remove our memoization of the
request, too.

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2024-11-29 16:17:22 +00:00
parent b29eaea119
commit 48cc7c5245
8 changed files with 130 additions and 78 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'yarn-plugin-backstage': patch
---
Use yarn's built-in http utilities for fetching release manifests
-1
View File
@@ -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(
+42 -9
View File
@@ -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(
-1
View File
@@ -47612,7 +47612,6 @@ __metadata:
"@yarnpkg/core": ^4.0.3
"@yarnpkg/fslib": ^3.0.2
"@yarnpkg/plugin-pack": ^4.0.0
lodash: ^4.17.21
nodemon: ^3.0.1
semver: ^7.6.0
languageName: unknown