yarn-plugin: include both backstage: and npm:

ranges in lockfile

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2025-01-07 19:39:06 +00:00
committed by Vincenzo Scamporlino
parent 3ebbc6b48f
commit fd70d8661e
10 changed files with 216 additions and 25 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'yarn-plugin-backstage': patch
---
Add both `npm:` and `backstage:` ranges to the lockfile to ensure compatibility with tools that parse the lockfile and ensure dependencies stay locked when building dist workspaces.
+1
View File
@@ -34,6 +34,7 @@
"@backstage/release-manifests": "workspace:^",
"@yarnpkg/core": "^4.0.3",
"@yarnpkg/fslib": "^3.0.2",
"@yarnpkg/plugin-npm": "^3.0.1",
"@yarnpkg/plugin-pack": "^4.0.0",
"semver": "^7.6.0"
},
@@ -107,7 +107,7 @@ describe('reduceDependency', () => {
),
project,
),
).rejects.toThrow(/unexpected version selector/i);
).rejects.toThrow(/invalid backstage: version range/i);
},
);
@@ -127,20 +127,31 @@ describe('reduceDependency', () => {
);
});
it('replaces the range with the corresponding npm package range', async () => {
await expect(
reduceDependency(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^',
),
project,
),
).resolves.toEqual(
it('adds the current Backstage version as a parameter on the range', async () => {
const result = await reduceDependency(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'npm:^6.7.8',
'backstage:^',
),
project,
);
await expect(
structUtils.parseRange(result.range).params?.backstage,
).toEqual('1.23.45');
});
it('adds the appropriate npm package version based on the Backstage manifest as a parameter on the range', async () => {
const result = await reduceDependency(
structUtils.makeDescriptor(
structUtils.makeIdent('backstage', 'core'),
'backstage:^',
),
project,
);
await expect(structUtils.parseRange(result.range).params?.npm).toEqual(
'6.7.8',
);
});
@@ -15,7 +15,7 @@
*/
import { Descriptor, Project, structUtils } from '@yarnpkg/core';
import { getPackageVersion } from '../util';
import { getCurrentBackstageVersion, getPackageVersion } from '../util';
import { PROTOCOL } from '../constants';
export const reduceDependency = async (
@@ -24,12 +24,18 @@ export const reduceDependency = async (
) => {
const range = structUtils.parseRange(dependency.range);
if (range.protocol === PROTOCOL) {
return structUtils.makeDescriptor(
dependency,
`npm:^${await getPackageVersion(dependency, project.configuration)}`,
if (range.protocol !== PROTOCOL) {
return dependency;
}
if (range.selector !== '^') {
throw new Error(
`Invalid backstage: version range found: ${dependency.range}`,
);
}
return dependency;
return structUtils.bindDescriptor(dependency, {
backstage: getCurrentBackstageVersion(),
npm: await getPackageVersion(dependency, project.configuration),
});
};
+9 -7
View File
@@ -67,10 +67,10 @@ const runYarnInstall = () =>
const nonWorkspaceEntries = (lockFileString: string = '') => {
const lockFile = yaml.parse(lockFileString);
const result: Record<string, string> = {};
const result = [];
for (const key of Object.keys(lockFile)) {
if (lockFile[key].version !== '0.0.0-use.local') {
result[key] = lockFile[key];
result.push(lockFile[key]);
}
}
@@ -232,15 +232,17 @@ describe('Backstage yarn plugin', () => {
'utf-8',
);
const lockFile = yaml.parse(lockFileContent);
const descriptors = Object.keys(yaml.parse(lockFileContent)).flatMap(
entry => entry.split(', '),
);
// Versions from old manifest no longer appear in lockfile
expect(lockFile['@backstage/cli-common@npm:^0.1.1']).toBeUndefined();
expect(lockFile['@backstage/config@npm:^0.1.2']).toBeUndefined();
expect(descriptors).not.toContain('@backstage/cli-common@npm:^0.1.1');
expect(descriptors).not.toContain('@backstage/config@npm:^0.1.2');
// Versions from new manifest have been added to lockfile
expect(lockFile['@backstage/cli-common@npm:^0.1.8']).toBeDefined();
expect(lockFile['@backstage/config@npm:^1.0.0']).toBeDefined();
expect(descriptors).toContain('@backstage/cli-common@npm:^0.1.8');
expect(descriptors).toContain('@backstage/config@npm:^1.0.0');
});
describe('after removing backstage:^ dependencies', () => {
+2
View File
@@ -24,6 +24,7 @@
import { Plugin, Hooks, semverUtils, YarnVersion } from '@yarnpkg/core';
import { Hooks as PackHooks } from '@yarnpkg/plugin-pack';
import { beforeWorkspacePacking, reduceDependency } from './handlers';
import { BackstageNpmResolver } from './resolvers';
// All dependencies of the yarn plugin are bundled during the build. Chalk
// triples the size of the plugin bundle when included, so we avoid the
@@ -48,6 +49,7 @@ const plugin: Plugin<Hooks & PackHooks> = {
reduceDependency,
beforeWorkspacePacking,
},
resolvers: [BackstageNpmResolver],
};
export default plugin;
@@ -0,0 +1,146 @@
/*
* Copyright 2024 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 {
structUtils,
Descriptor,
Locator,
Package,
Resolver,
ResolveOptions,
} from '@yarnpkg/core';
import { NpmSemverResolver } from '@yarnpkg/plugin-npm';
import { PROTOCOL } from '../constants';
export class BackstageNpmResolver implements Resolver {
static protocol = PROTOCOL;
/**
* Target only descriptors using the `backstage:` protocol
*/
supportsDescriptor = (descriptor: Descriptor) =>
descriptor.range.startsWith(BackstageNpmResolver.protocol);
/**
* We treat any `backstage:` descriptor as if it's targeting the npm package
* version from the manifest for the current version of Backstage, by pulling
* in the `NpmSemverResolver` and deferring to its `getCandidates` method.
*
* The version itself comes from the `npm` parameter on the incoming
* descriptor, which is set by the `reduceDependency` hook.
*/
async getCandidates(
descriptor: Descriptor,
dependencies: Record<string, Package>,
opts: ResolveOptions,
): Promise<Locator[]> {
const npmVersion = structUtils.parseRange(descriptor.range).params?.npm;
if (!npmVersion || Array.isArray(npmVersion)) {
throw new Error(
`Missing npm parameter on backstage: range "${descriptor.range}"`,
);
}
return new NpmSemverResolver().getCandidates(
structUtils.makeDescriptor(descriptor, `npm:^${npmVersion}`),
dependencies,
opts,
);
}
/**
* We insert the `npm:^<version>` descriptor as an additional dependency to
* ensure that dependencies remain locked when adding and removing the plugin
* from repositories. This is relevant for example when building packed
* production-like workspaces for testing using `backstage-cli
* build-workspace`.
*/
getResolutionDependencies(
descriptor: Descriptor,
): Record<string, Descriptor> {
const npmVersion = structUtils.parseRange(descriptor.range).params?.npm;
if (!npmVersion) {
throw new Error(`Unreachable`);
}
return {
[structUtils.stringifyIdent(descriptor)]: structUtils.makeDescriptor(
descriptor,
`npm:^${npmVersion}`,
),
};
}
/**
* This method is called when deduplicating locators. We first convert any
* `backstage:` locators into the corresponding `npm:` locator, and then defer
* to the implementation from `NpmSemverResolver`.
*/
async getSatisfying(
descriptor: Descriptor,
dependencies: Record<string, Package>,
locators: Array<Locator>,
opts: ResolveOptions,
) {
let npmDescriptor = descriptor;
const range = structUtils.parseRange(npmDescriptor.range);
if (range.protocol === PROTOCOL) {
const npmVersion = range.params?.npm;
npmDescriptor = structUtils.makeDescriptor(
descriptor,
`npm:^${npmVersion}`,
);
}
return new NpmSemverResolver().getSatisfying(
npmDescriptor,
dependencies,
locators,
opts,
);
}
/**
* Stub - no descriptor binding is needed in this resolver (note though that
* it does rely on the binding performed in the `reduceDependency` hook).
*/
bindDescriptor = (descriptor: Descriptor) => descriptor;
/**
* This plugin does not need to support any locators itself, since the
* `getCandidates` method will always convert `backstage:` versions into
* `npm:` versions which are resolved using the `NpmSemverResolver`.
*/
supportsLocator = () => false;
/**
* This method should never be called, since all emitted locators use the
* `npm:` protocol.
*/
shouldPersistResolution = () => {
throw new Error('Unreachable');
};
/**
* This method should never be called, since all emitted locators use the
* `npm:` protocol.
*/
resolve = async () => {
throw new Error(`Unreachable`);
};
}
@@ -0,0 +1,16 @@
/*
* Copyright 2025 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.
*/
export { BackstageNpmResolver } from './BackstageNpmResolver';
+1
View File
@@ -14,4 +14,5 @@
* limitations under the License.
*/
export { getCurrentBackstageVersion } from './getCurrentBackstageVersion';
export { getPackageVersion } from './getPackageVersion';
+1
View File
@@ -48448,6 +48448,7 @@ __metadata:
"@yarnpkg/builder": "npm:^4.0.0"
"@yarnpkg/core": "npm:^4.0.3"
"@yarnpkg/fslib": "npm:^3.0.2"
"@yarnpkg/plugin-npm": "npm:^3.0.1"
"@yarnpkg/plugin-pack": "npm:^4.0.0"
fs-extra: "npm:^11.2.0"
nodemon: "npm:^3.0.1"