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:
@@ -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