From ac91864de30ef860c88f0519a65053acef05e233 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Thu, 12 Dec 2024 20:33:58 +0000 Subject: [PATCH] yarn-plugin: rewrite using `reduceDependency` hook Switching to using the reduceDependency hook simplifies the plugin, since we can do all the work in one go. It also means that the yarn.lock file doesn't need to change when adding and removing the yarn plugin, which avoids unintentionally unlocking packages versions. As part of this change, the descriptor emitted by the plugin has been changed from `npm:` to `npm:^`, to match the range emitted when packing packages. This ensures good interoperability with the `backstage-cli build-workspace`, which packs packages into a new workspace and copies yarn.lock across. Signed-off-by: MT Lewis --- .changeset/nice-kangaroos-rush.md | 6 + packages/yarn-plugin/package.json | 1 + packages/yarn-plugin/report.api.md | 5 +- .../src/handlers/beforeWorkspacePacking.ts | 10 +- packages/yarn-plugin/src/handlers/index.ts | 18 ++ .../src/handlers/reduceDependency.test.ts | 209 ++++++++++++ .../src/handlers/reduceDependency.ts | 35 ++ packages/yarn-plugin/src/index.ts | 11 +- .../src/resolver/BackstageResolver.test.ts | 301 ------------------ .../src/resolver/BackstageResolver.ts | 128 -------- packages/yarn-plugin/src/util.ts | 28 +- yarn.lock | 1 + 12 files changed, 288 insertions(+), 465 deletions(-) create mode 100644 .changeset/nice-kangaroos-rush.md create mode 100644 packages/yarn-plugin/src/handlers/index.ts create mode 100644 packages/yarn-plugin/src/handlers/reduceDependency.test.ts create mode 100644 packages/yarn-plugin/src/handlers/reduceDependency.ts delete mode 100644 packages/yarn-plugin/src/resolver/BackstageResolver.test.ts delete mode 100644 packages/yarn-plugin/src/resolver/BackstageResolver.ts diff --git a/.changeset/nice-kangaroos-rush.md b/.changeset/nice-kangaroos-rush.md new file mode 100644 index 0000000000..942fc5b4e6 --- /dev/null +++ b/.changeset/nice-kangaroos-rush.md @@ -0,0 +1,6 @@ +--- +'yarn-plugin-backstage': patch +--- + +Switch to using `reduceDependency` hook to replace `backstage:^` versions. This +makes the same yarn.lock file valid whether or not the plugin is installed. diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index bca6fed42f..b327fb494e 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -41,6 +41,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@yarnpkg/builder": "^4.0.0", + "fs-extra": "^11.2.0", "nodemon": "^3.0.1" } } diff --git a/packages/yarn-plugin/report.api.md b/packages/yarn-plugin/report.api.md index 928703ed3e..620bbbb567 100644 --- a/packages/yarn-plugin/report.api.md +++ b/packages/yarn-plugin/report.api.md @@ -3,10 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { Hooks } from '@yarnpkg/plugin-pack'; +import { Hooks } from '@yarnpkg/core'; +import { Hooks as Hooks_2 } from '@yarnpkg/plugin-pack'; import { Plugin as Plugin_2 } from '@yarnpkg/core'; // @public (undocumented) -const plugin: Plugin_2; +const plugin: Plugin_2; export default plugin; ``` diff --git a/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts index e5823607e3..436c949b1e 100644 --- a/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts +++ b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts @@ -15,11 +15,7 @@ */ import { Descriptor, Workspace, structUtils } from '@yarnpkg/core'; -import { - bindBackstageVersion, - getCurrentBackstageVersion, - getPackageVersion, -} from '../util'; +import { getPackageVersion } from '../util'; import { PROTOCOL } from '../constants'; const hasBackstageVersion = (range: string) => @@ -45,8 +41,6 @@ export const beforeWorkspacePacking = async ( workspace: Workspace, rawManifest: any, ) => { - const backstageVersion = getCurrentBackstageVersion(); - for (const dependencyType of ['dependencies', 'devDependencies'] as const) { const entries = Array.from( workspace.manifest.getForScope(dependencyType).values(), @@ -69,7 +63,7 @@ export const beforeWorkspacePacking = async ( ); rawManifest[finalDependencyType][ident] = `^${await getPackageVersion( - bindBackstageVersion(descriptor, backstageVersion), + descriptor, workspace.project.configuration, )}`; } diff --git a/packages/yarn-plugin/src/handlers/index.ts b/packages/yarn-plugin/src/handlers/index.ts new file mode 100644 index 0000000000..baa144b36f --- /dev/null +++ b/packages/yarn-plugin/src/handlers/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { beforeWorkspacePacking } from './beforeWorkspacePacking'; +export { reduceDependency } from './reduceDependency'; diff --git a/packages/yarn-plugin/src/handlers/reduceDependency.test.ts b/packages/yarn-plugin/src/handlers/reduceDependency.test.ts new file mode 100644 index 0000000000..568f39653a --- /dev/null +++ b/packages/yarn-plugin/src/handlers/reduceDependency.test.ts @@ -0,0 +1,209 @@ +/* + * 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 { join as joinPath } from 'path'; +import fs from 'fs-extra'; +import { Configuration, Project, structUtils, httpUtils } from '@yarnpkg/core'; +import { npath, ppath } from '@yarnpkg/fslib'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { reduceDependency } from './reduceDependency'; + +describe('reduceDependency', () => { + const mockDir = createMockDirectory(); + let project: Project; + let getMock: jest.SpyInstance< + ReturnType<(typeof httpUtils)['get']>, + Parameters<(typeof httpUtils)['get']> + >; + + beforeEach(() => { + project = new Project(ppath.cwd(), { + configuration: Configuration.create(ppath.cwd()), + }); + + jest + .spyOn(ppath, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + jest + .spyOn(process, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + getMock = 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', + }), + 'package.json': JSON.stringify({ + workspaces: { + packages: ['packages/*'], + }, + }), + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': 'backstage:^', + }, + }), + }, + }, + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it.each` + range + ${'npm:1.2.3'} + ${'link:../foo/bar'} + ${'workspace:^'} + `('returns non-backstage range "$range" unchanged', async ({ range }) => { + const descriptor = structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + range, + ); + + await expect(reduceDependency(descriptor, project)).resolves.toEqual( + descriptor, + ); + }); + + it.each` + selector + ${'*'} + ${'latest'} + `( + 'rejects backstage: ranges with invalid selector "$selector"', + async ({ selector }) => { + await expect( + reduceDependency( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + `backstage:${selector}`, + ), + project, + ), + ).rejects.toThrow(/unexpected version selector/i); + }, + ); + + describe('with range "backstage:^"', () => { + it('loads the manifest for the current Backstage version', async () => { + await reduceDependency( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:^', + ), + project, + ); + + expect(getMock).toHaveBeenCalledWith( + 'https://versions.backstage.io/v1/releases/1.23.45/manifest.json', + expect.anything(), + ); + }); + + it('replaces the range with the corresponding npm package range', async () => { + await expect( + reduceDependency( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:^', + ), + project, + ), + ).resolves.toEqual( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'npm:^6.7.8', + ), + ); + }); + + it('throws if backstage.json is missing', async () => { + await fs.remove(joinPath(mockDir.path, 'backstage.json')); + + await expect( + reduceDependency( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'other'), + 'backstage:^', + ), + project, + ), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Valid version string not found in backstage.json', + ), + }), + ); + }); + + it(`throws if backstage.json doesn't contain a valid version`, async () => { + mockDir.addContent({ + 'backstage.json': JSON.stringify({}), + }); + + await expect( + reduceDependency( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'other'), + 'backstage:^', + ), + project, + ), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Valid version string not found in backstage.json', + ), + }), + ); + }); + + it(`throws for packages that don't appear in the manifest`, async () => { + await expect( + reduceDependency( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'other'), + 'backstage:^', + ), + project, + ), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Package @backstage/other not found in manifest for Backstage v1.23.45', + ), + }), + ); + }); + }); +}); diff --git a/packages/yarn-plugin/src/handlers/reduceDependency.ts b/packages/yarn-plugin/src/handlers/reduceDependency.ts new file mode 100644 index 0000000000..e00a39c27c --- /dev/null +++ b/packages/yarn-plugin/src/handlers/reduceDependency.ts @@ -0,0 +1,35 @@ +/* + * 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 { Descriptor, Project, structUtils } from '@yarnpkg/core'; +import { getPackageVersion } from '../util'; +import { PROTOCOL } from '../constants'; + +export const reduceDependency = async ( + dependency: Descriptor, + project: Project, +) => { + const range = structUtils.parseRange(dependency.range); + + if (range.protocol === PROTOCOL) { + return structUtils.makeDescriptor( + dependency, + `npm:^${await getPackageVersion(dependency, project.configuration)}`, + ); + } + + return dependency; +}; diff --git a/packages/yarn-plugin/src/index.ts b/packages/yarn-plugin/src/index.ts index 9c20e9a45c..dcf2fe6943 100644 --- a/packages/yarn-plugin/src/index.ts +++ b/packages/yarn-plugin/src/index.ts @@ -21,10 +21,9 @@ * @packageDocumentation */ -import { Plugin, semverUtils, YarnVersion } from '@yarnpkg/core'; -import { Hooks } from '@yarnpkg/plugin-pack'; -import { beforeWorkspacePacking } from './handlers/beforeWorkspacePacking'; -import { BackstageResolver } from './resolver/BackstageResolver'; +import { Plugin, Hooks, semverUtils, YarnVersion } from '@yarnpkg/core'; +import { Hooks as PackHooks } from '@yarnpkg/plugin-pack'; +import { beforeWorkspacePacking, reduceDependency } from './handlers'; // 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 @@ -44,11 +43,11 @@ if (!semverUtils.satisfiesWithPrereleases(YarnVersion, '^4.1.1')) { /** * @public */ -const plugin: Plugin = { +const plugin: Plugin = { hooks: { + reduceDependency, beforeWorkspacePacking, }, - resolvers: [BackstageResolver], }; export default plugin; diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts deleted file mode 100644 index 8cba189cb8..0000000000 --- a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts +++ /dev/null @@ -1,301 +0,0 @@ -/* - * 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 { - Configuration, - Project, - ResolveOptions, - structUtils, - ThrowReport, - httpUtils, -} from '@yarnpkg/core'; -import { npath, ppath } from '@yarnpkg/fslib'; -import { BackstageResolver } from './BackstageResolver'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -describe('BackstageResolver', () => { - const mockDir = createMockDirectory(); - let backstageResolver: BackstageResolver; - let resolveOptions: ResolveOptions; - - beforeEach(() => { - jest - .spyOn(ppath, 'cwd') - .mockReturnValue(npath.toPortablePath(mockDir.path)); - - jest - .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', - }), - 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, - }), - packages: { - a: { - 'package.json': JSON.stringify({ - name: 'a', - dependencies: { - '@backstage/core': 'backstage:^', - }, - }), - }, - }, - }); - - backstageResolver = new BackstageResolver(); - - resolveOptions = { - resolver: backstageResolver, - project: new Project(ppath.cwd(), { - configuration: Configuration.create(ppath.cwd()), - }), - report: new ThrowReport(), - }; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('supportsDescriptor', () => { - it.each([ - ['backstage:^'], - ['backstage:1.26.0-next.3'], - ['backstage:anything'], - ])('returns true for range "%s"', range => { - expect( - backstageResolver.supportsDescriptor( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - range, - ), - ), - ).toEqual(true); - }); - }); - - describe('bindDescriptor', () => { - describe('with range "backstage:^"', () => { - it('returns a descriptor with a version range for the current Backstage version', () => { - expect( - backstageResolver.bindDescriptor( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'backstage:^', - ), - ), - ).toEqual( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'backstage:^::v=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:^::v=1.23.45', - ), - ), - ).toEqual( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'backstage:^::v=1.23.45', - ), - ); - }); - }); - }); - - describe('getCandidates', () => { - it('returns an npm: descriptor based on the manifest for the appropriate backstage version', async () => { - const descriptor = structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'backstage:^::v=1.23.45', - ); - - await expect( - backstageResolver.getCandidates(descriptor, {}, resolveOptions), - ).resolves.toEqual([structUtils.makeLocator(descriptor, 'npm:6.7.8')]); - }); - - it('rejects descriptors not using the backstage: protocol', async () => { - await expect( - backstageResolver.getCandidates( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'npm:1.2.3', - ), - {}, - resolveOptions, - ), - ).rejects.toThrow(/unsupported version protocol/i); - }); - - it('rejects backstage: ranges missing a version parameter', async () => { - await expect( - backstageResolver.getCandidates( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'backstage:^', - ), - {}, - resolveOptions, - ), - ).rejects.toThrow(/missing Backstage version/i); - }); - - it('rejects backstage: ranges with multiple version parameters', async () => { - await expect( - backstageResolver.getCandidates( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'backstage:^::v=1&v=2', - ), - {}, - resolveOptions, - ), - ).rejects.toThrow(/multiple Backstage versions/i); - }); - - 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}`, - ), - {}, - resolveOptions, - ), - ).rejects.toThrow(/unexpected version selector/i); - }, - ); - }); - - describe('getSatisfying', () => { - it('filters out locators for other packages', async () => { - await expect( - backstageResolver.getSatisfying( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'backstage:^::v=1.23.45', - ), - {}, - [ - structUtils.makeLocator( - structUtils.makeIdent('backstage', 'foo'), - 'npm:1.2.3', - ), - structUtils.makeLocator( - structUtils.makeIdent('backstage', 'core'), - 'npm:6.7.8', - ), - structUtils.makeLocator( - structUtils.makeIdent('backstage', 'bar'), - 'npm:1.2.3', - ), - ], - resolveOptions, - ), - ).resolves.toEqual({ - locators: [ - structUtils.makeLocator( - structUtils.makeIdent('backstage', 'core'), - 'npm:6.7.8', - ), - ], - sorted: true, - }); - }); - - it('filters out locators for other package versions', async () => { - await expect( - backstageResolver.getSatisfying( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'backstage:^::v=1.23.45', - ), - {}, - [ - structUtils.makeLocator( - structUtils.makeIdent('backstage', 'core'), - 'npm:5.6.7', - ), - structUtils.makeLocator( - structUtils.makeIdent('backstage', 'core'), - 'npm:6.7.8', - ), - structUtils.makeLocator( - structUtils.makeIdent('backstage', 'bar'), - 'npm:7.8.9', - ), - ], - resolveOptions, - ), - ).resolves.toEqual({ - locators: [ - structUtils.makeLocator( - structUtils.makeIdent('backstage', 'core'), - 'npm:6.7.8', - ), - ], - sorted: true, - }); - }); - - it('throws for non `backstage:` descriptors', async () => { - await expect( - backstageResolver.getSatisfying( - structUtils.makeDescriptor( - structUtils.makeIdent('backstage', 'core'), - 'npm:1.2.3', - ), - {}, - [], - resolveOptions, - ), - ).rejects.toThrow(/unsupported version protocol/i); - }); - }); -}); diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.ts deleted file mode 100644 index d2d0c98d1e..0000000000 --- a/packages/yarn-plugin/src/resolver/BackstageResolver.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 { PROTOCOL } from '../constants'; -import { - bindBackstageVersion, - getCurrentBackstageVersion, - getPackageVersion, -} from '../util'; - -export class BackstageResolver implements Resolver { - static protocol = PROTOCOL; - - supportsDescriptor = (descriptor: Descriptor) => - descriptor.range.startsWith(BackstageResolver.protocol); - - shouldPersistResolution = () => true; - - /** - * Called for each dependency present in the dependency list of a package - * definition. If it returns a new descriptor, this new descriptor will be - * used. - * - * In this plugin, we convert the specific range "backstage:^" to - * "backstage:". This new range will be the one - * stored in lockfile entries, ensuring that we re-resolve the package when - * the version in backstage.json changes. - */ - bindDescriptor(descriptor: Descriptor): Descriptor { - if (descriptor.range === 'backstage:^') { - return bindBackstageVersion(descriptor, getCurrentBackstageVersion()); - } - - return descriptor; - } - - /** - * Given a descriptor, return the list of locators that potentially satisfy - * it. The implementation in this plugin converts a `backstage:` range with a - * concrete version into the appropriate concrete npm version for that - * backstage release. - */ - async getCandidates( - descriptor: Descriptor, - _dependencies: Record, - opts: ResolveOptions, - ): Promise { - return [ - structUtils.makeLocator( - descriptor, - `npm:${await getPackageVersion( - descriptor, - opts.project.configuration, - )}`, - ), - ]; - } - - /** - * Given a descriptor and a list of possible locators, return a filtered list - * containing only locators that satisfy the descriptor. Since each Backstage - * release version corresponds to a single version for each package, we just - * need to filter that array for locators with that exact version. - */ - async getSatisfying( - descriptor: Descriptor, - _dependencies: Record, - locators: Array, - opts: ResolveOptions, - ): Promise<{ locators: Locator[]; sorted: boolean }> { - const packageVersion = await getPackageVersion( - descriptor, - opts.project.configuration, - ); - - return { - locators: locators.filter( - locator => - structUtils.areIdentsEqual(descriptor, locator) && - locator.reference === `npm:${packageVersion}`, - ), - sorted: true, - }; - } - - /** - * This plugin does not need to support any locators itself, since the - * `getCandidates` method will always convert `backstage:` versions into - * `npm:` versions which can be handled as usual. - */ - supportsLocator = () => false; - - /** - * This resolver never transforms the packages that are actually depended on, - * only replaces versions. As such there's never a need to add additional - * dependencies. - */ - getResolutionDependencies = () => ({}); - - /** - * Once transformed into locators (through getCandidates), the versions are - * resolved by the NpmSemverResolver - */ - async resolve(): Promise { - throw new Error(`Unreachable`); - } -} diff --git a/packages/yarn-plugin/src/util.ts b/packages/yarn-plugin/src/util.ts index 23ce55ac80..939ce807e5 100644 --- a/packages/yarn-plugin/src/util.ts +++ b/packages/yarn-plugin/src/util.ts @@ -31,6 +31,11 @@ export const getCurrentBackstageVersion = () => { const workspaceRoot = npath.toPortablePath( findPaths(npath.fromPortablePath(ppath.cwd())).targetRoot, ); + const backstageJsonPath = ppath.join(workspaceRoot, BACKSTAGE_JSON); + + if (!xfs.existsSync(backstageJsonPath)) { + throw new Error('Valid version string not found in backstage.json'); + } const backstageJson = xfs.readJsonSync( ppath.join(workspaceRoot, BACKSTAGE_JSON), @@ -45,13 +50,6 @@ export const getCurrentBackstageVersion = () => { return backstageVersion; }; -export const bindBackstageVersion = ( - descriptor: Descriptor, - backstageVersion: string, -) => { - return structUtils.bindDescriptor(descriptor, { v: backstageVersion }); -}; - export const getPackageVersion = async ( descriptor: Descriptor, configuration: Configuration, @@ -71,20 +69,10 @@ export const getPackageVersion = async ( ); } - 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 backstageVersion = getCurrentBackstageVersion(); const manifest = await getManifestByVersion({ - version: range.params.v, + version: backstageVersion, // 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: @@ -123,7 +111,7 @@ export const getPackageVersion = async ( if (!manifestEntry) { throw new Error( - `Package ${ident} not found in manifest for Backstage v${range.selector}. ` + + `Package ${ident} not found in manifest for Backstage v${backstageVersion}. ` + `This means the specified package is not included in this Backstage ` + `release. This may imply the package has been replaced with an alternative - ` + `please review the documentation for the package. If you need to continue ` + diff --git a/yarn.lock b/yarn.lock index afc08ad8a0..b556c34400 100644 --- a/yarn.lock +++ b/yarn.lock @@ -48206,6 +48206,7 @@ __metadata: "@yarnpkg/core": ^4.0.3 "@yarnpkg/fslib": ^3.0.2 "@yarnpkg/plugin-pack": ^4.0.0 + fs-extra: ^11.2.0 nodemon: ^3.0.1 semver: ^7.6.0 languageName: unknown