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:
committed by
Vincenzo Scamporlino
parent
3ebbc6b48f
commit
fd70d8661e
@@ -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.
|
||||
@@ -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),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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';
|
||||
@@ -14,4 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { getCurrentBackstageVersion } from './getCurrentBackstageVersion';
|
||||
export { getPackageVersion } from './getPackageVersion';
|
||||
|
||||
Reference in New Issue
Block a user