yarn-plugin: move Backstage version to parameters in bound descriptors

The previous version of this code transformed
the version range "backstage:^" to
"backstage:<version-from-backstage.json>" when
writing entries in yarn.lock. This does seem to be
something that the yarn plugin API supports, but
it turns out to cause issues in some third-party
tools which assume that version ranges will be
consistent between package.json and yarn.lock.

To fix this issue, we're now leveraging the
descriptor binding feature in Backstage (which I
didn't know about before) to append a descriptor
parameter containing the Backstage version. This
keeps the selector of the descriptors consistent
between package.json and yarn.lock entries, which
will hopefully mean that this plugin no longer
breaks other tools.

cf. https://github.com/snyk/nodejs-lockfile-parser/issues/256

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2024-11-19 20:19:13 +00:00
parent 42c2c95f5b
commit 901748188a
4 changed files with 71 additions and 57 deletions
@@ -16,7 +16,11 @@
import { Descriptor, Workspace, structUtils } from '@yarnpkg/core';
import { some } from 'lodash';
import { getCurrentBackstageVersion, getPackageVersion } from '../util';
import {
bindBackstageVersion,
getCurrentBackstageVersion,
getPackageVersion,
} from '../util';
import { PROTOCOL } from '../constants';
const hasBackstageVersion = (range: string) =>
@@ -66,10 +70,7 @@ export const beforeWorkspacePacking = async (
);
rawManifest[finalDependencyType][ident] = `^${await getPackageVersion(
structUtils.makeDescriptor(
descriptor,
`${PROTOCOL}${backstageVersion}`,
),
bindBackstageVersion(descriptor, backstageVersion),
)}`;
}
}
@@ -96,7 +96,7 @@ describe('BackstageResolver', () => {
describe('bindDescriptor', () => {
describe('with range "backstage:^"', () => {
it('returns a descriptor basedwith a version range for the current Backstage version', () => {
it('returns a descriptor with a version range for the current Backstage version', () => {
expect(
backstageResolver.bindDescriptor(
structUtils.makeDescriptor(
@@ -107,25 +107,25 @@ describe('BackstageResolver', () => {
).toEqual(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:1.23.45',
'backstage:^::v=1.23.45',
),
);
});
});
describe('with range "backstage:1.23.45"', () => {
describe('with range "backstage:^::v=1.23.45"', () => {
it('returns the correct descriptor', () => {
expect(
backstageResolver.bindDescriptor(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:1.23.45',
'backstage:^::v=1.23.45',
),
),
).toEqual(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:1.23.45',
'backstage:^::v=1.23.45',
),
);
});
@@ -136,7 +136,7 @@ describe('BackstageResolver', () => {
it('returns an npm: descriptor based on the manifest for the appropriate backstage version', async () => {
const descriptor = structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:1.23.45',
'backstage:^::v=1.23.45',
);
await expect(
@@ -155,7 +155,7 @@ describe('BackstageResolver', () => {
).rejects.toThrow(/unsupported version protocol/i);
});
it('rejects backstage: ranges with a ^ shorthand version', async () => {
it('rejects backstage: ranges missing a version parameter', async () => {
await expect(
backstageResolver.getCandidates(
structUtils.makeDescriptor(
@@ -163,35 +163,42 @@ describe('BackstageResolver', () => {
'backstage:^',
),
),
).rejects.toThrow(/invalid backstage version/i);
).rejects.toThrow(/missing Backstage version/i);
});
it('rejects backstage: ranges with a * shorthand version', async () => {
it('rejects backstage: ranges with multiple version parameters', async () => {
await expect(
backstageResolver.getCandidates(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:*',
'backstage:^::v=1&v=2',
),
),
).rejects.toThrow(/invalid backstage version/i);
).rejects.toThrow(/multiple Backstage versions/i);
});
it('rejects backstage: ranges with an invalid version specified', async () => {
await expect(
backstageResolver.getCandidates(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:latest',
it.each`
selector
${'*'}
${'latest'}
`(
'rejects backstage: ranges with invalid selector "$selector"',
async ({ selector }) => {
await expect(
backstageResolver.getCandidates(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
`backstage:${selector}`,
),
),
),
).rejects.toThrow(/invalid backstage version/i);
});
).rejects.toThrow(/unexpected version selector/i);
},
);
it('memoizes manifest retrieval', async () => {
const descriptor1 = structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:1.23.45',
'backstage:^::v=1.23.45',
);
for (let i = 0; i < 5; i++) {
@@ -202,7 +209,7 @@ describe('BackstageResolver', () => {
const descriptor2 = structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:6.78.90',
'backstage:^::v=6.78.90',
);
for (let i = 0; i < 5; i++) {
@@ -219,7 +226,7 @@ describe('BackstageResolver', () => {
backstageResolver.getSatisfying(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:1.23.45',
'backstage:^::v=1.23.45',
),
{},
[
@@ -253,7 +260,7 @@ describe('BackstageResolver', () => {
backstageResolver.getSatisfying(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:1.23.45',
'backstage:^::v=1.23.45',
),
{},
[
@@ -292,7 +299,7 @@ describe('BackstageResolver', () => {
{},
[],
),
).rejects.toThrow(/unexpected npm: range/i);
).rejects.toThrow(/unsupported version protocol/i);
});
});
});
@@ -21,9 +21,12 @@ import {
Package,
Resolver,
} from '@yarnpkg/core';
import semver from 'semver';
import { PROTOCOL } from '../constants';
import { getCurrentBackstageVersion, getPackageVersion } from '../util';
import {
bindBackstageVersion,
getCurrentBackstageVersion,
getPackageVersion,
} from '../util';
export class BackstageResolver implements Resolver {
static protocol = PROTOCOL;
@@ -45,10 +48,7 @@ export class BackstageResolver implements Resolver {
*/
bindDescriptor(descriptor: Descriptor): Descriptor {
if (descriptor.range === 'backstage:^') {
return structUtils.makeDescriptor(
descriptor,
`${PROTOCOL}${getCurrentBackstageVersion()}`,
);
return bindBackstageVersion(descriptor, getCurrentBackstageVersion());
}
return descriptor;
@@ -61,23 +61,6 @@ export class BackstageResolver implements Resolver {
* backstage release.
*/
async getCandidates(descriptor: Descriptor): Promise<Locator[]> {
const range = structUtils.parseRange(descriptor.range);
if (range.protocol !== BackstageResolver.protocol) {
throw new Error(
`Unsupported version protocol in version range "${
descriptor.range
}" for package ${structUtils.stringifyIdent(descriptor)}`,
);
}
if (!semver.valid(range.selector)) {
throw new Error(
`Invalid Backstage version string when resolving version for ${structUtils.stringifyIdent(
descriptor,
)}`,
);
}
return [
structUtils.makeLocator(
descriptor,
+27 -4
View File
@@ -43,20 +43,43 @@ export const getCurrentBackstageVersion = () => {
return backstageVersion;
};
export const bindBackstageVersion = (
descriptor: Descriptor,
backstageVersion: string,
) => {
return structUtils.bindDescriptor(descriptor, { v: backstageVersion });
};
export const getPackageVersion = async (descriptor: Descriptor) => {
const ident = structUtils.stringifyIdent(descriptor);
const range = structUtils.parseRange(descriptor.range);
if (range.protocol !== PROTOCOL) {
throw new Error(`Unexpected ${range.protocol} range when packing`);
throw new Error(
`Unsupported version protocol in version range "${descriptor.range}" for package ${ident}`,
);
}
if (!semverValid(range.selector)) {
throw new Error(`Missing backstage version in range ${descriptor.range}`);
if (range.selector !== '^') {
throw new Error(
`Unexpected version selector "${range.selector}" for package ${ident}`,
);
}
if (!range.params?.v) {
throw new Error(
`Missing Backstage version parameter in range "${descriptor.range}" for package ${ident}`,
);
}
if (Array.isArray(range.params.v)) {
throw new Error(
`Multiple Backstage versions specified in range "${descriptor.range}" for package ${ident}`,
);
}
const manifest = await getManifestByVersion({
version: range.selector,
version: range.params.v,
});
const manifestEntry = manifest.packages.find(