diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index 713d7e5c49..817497a770 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -1,42 +1,43 @@ { "name": "yarn-plugin-backstage", - "description": "Yarn plugin for working with Backstage monorepos", "version": "0.0.0", - "private": true, + "description": "Yarn plugin for working with Backstage monorepos", "backstage": { "role": "node-library" }, + "private": true, + "keywords": [ + "backstage" + ], "homepage": "https://backstage.io", "repository": { "type": "git", "url": "https://github.com/backstage/backstage", "directory": "packages/yarn-plugin" }, - "keywords": [ - "backstage" - ], "license": "Apache-2.0", "main": "./src/index.ts", "scripts": { "build": "builder build plugin", - "lint": "backstage-cli package lint", - "test": "backstage-cli package test", "clean": "backstage-cli package clean", - "start": "nodemon --" + "lint": "backstage-cli package lint", + "start": "nodemon --", + "test": "backstage-cli package test" + }, + "nodemonConfig": { + "exec": "builder build plugin", + "ext": "ts", + "watch": "./src" }, "dependencies": { "@backstage/release-manifests": "workspace:^", "@yarnpkg/core": "^4.0.3", - "@yarnpkg/fslib": "^3.0.2" + "@yarnpkg/fslib": "^3.0.2", + "semver": "^7.6.0" }, "devDependencies": { "@backstage/cli": "workspace:^", "@yarnpkg/builder": "^4.0.0", "nodemon": "^3.0.1" - }, - "nodemonConfig": { - "watch": "./src", - "exec": "builder build plugin", - "ext": "ts" } } diff --git a/packages/yarn-plugin/src/constants.ts b/packages/yarn-plugin/src/constants.ts new file mode 100644 index 0000000000..4473b1e347 --- /dev/null +++ b/packages/yarn-plugin/src/constants.ts @@ -0,0 +1,17 @@ +/* + * 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 const PROTOCOL = 'backstage:'; diff --git a/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts new file mode 100644 index 0000000000..80e663f17f --- /dev/null +++ b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts @@ -0,0 +1,66 @@ +/* + * 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, Workspace, structUtils } from '@yarnpkg/core'; +import { inferPackageVersion } from '../util'; +import { PROTOCOL } from '../constants'; + +const getFinalDependencyType = ( + dependencyType: string, + descriptor: Descriptor, + workspace: Workspace, +) => { + if (dependencyType !== 'dependencies') { + return dependencyType; + } + + return workspace.manifest.ensureDependencyMeta( + structUtils.makeDescriptor(descriptor, 'unknown'), + ).optional + ? 'optionalDependencies' + : dependencyType; +}; + +export const beforeWorkspacePacking = async ( + workspace: Workspace, + rawManifest: any, +) => { + for (const dependencyType of [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + ] as const) { + const entries = Array.from( + workspace.manifest.getForScope(dependencyType).values(), + ).filter( + descriptor => + structUtils.parseRange(descriptor.range).protocol === PROTOCOL, + ); + + for (const descriptor of entries) { + const finalDependencyType = getFinalDependencyType( + dependencyType, + descriptor, + workspace, + ); + const ident = structUtils.stringifyIdent(descriptor); + + rawManifest[finalDependencyType][ident] = await inferPackageVersion( + descriptor, + ); + } + } +}; diff --git a/packages/yarn-plugin/src/index.ts b/packages/yarn-plugin/src/index.ts index aac855fadf..6c1c805fd8 100644 --- a/packages/yarn-plugin/src/index.ts +++ b/packages/yarn-plugin/src/index.ts @@ -22,12 +22,16 @@ */ import { Plugin } from '@yarnpkg/core'; +import { beforeWorkspacePacking } from './handlers/beforeWorkspacePacking'; import { BackstageResolver } from './resolver/BackstageResolver'; /** * @public */ const plugin: Plugin = { + hooks: { + beforeWorkspacePacking, + }, resolvers: [BackstageResolver], }; diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.ts index 8a28c481ed..2bb16f8525 100644 --- a/packages/yarn-plugin/src/resolver/BackstageResolver.ts +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.ts @@ -21,11 +21,11 @@ import { Package, Resolver, } from '@yarnpkg/core'; -import { xfs, npath } from '@yarnpkg/fslib'; -import { getManifestByVersion } from '@backstage/release-manifests'; +import { PROTOCOL } from '../constants'; +import { inferBackstageVersion, inferPackageVersion } from '../util'; export class BackstageResolver implements Resolver { - static protocol = `backstage:`; + static protocol = PROTOCOL; supportsDescriptor = (descriptor: Descriptor) => descriptor.range.startsWith(BackstageResolver.protocol); @@ -33,39 +33,18 @@ export class BackstageResolver implements Resolver { shouldPersistResolution = () => true; bindDescriptor(descriptor: Descriptor): Descriptor { - if (descriptor.range === `${BackstageResolver.protocol}*`) { - const backstageJson = xfs.readJsonSync( - npath.toPortablePath('./backstage.json'), - ); - - return structUtils.makeDescriptor( - descriptor, - `backstage:${backstageJson.version}`, - ); - } - - return descriptor; + return structUtils.makeDescriptor( + descriptor, + `backstage:${inferBackstageVersion(descriptor)}`, + ); } async getCandidates(descriptor: Descriptor): Promise { - const backstageVersion = descriptor.range.replace( - BackstageResolver.protocol, - '', - ); - - const manifest = await getManifestByVersion({ version: backstageVersion }); - const ident = structUtils.stringifyIdent(descriptor); - - const manifestEntry = manifest.packages.find( - candidate => candidate.name === ident, - ); - - if (!manifestEntry) { - throw new Error(`Package ${ident} not found in manifest`); - } - return [ - structUtils.makeLocator(descriptor, `npm:${manifestEntry.version}`), + structUtils.makeLocator( + descriptor, + `npm:${await inferPackageVersion(descriptor)}`, + ), ]; } diff --git a/packages/yarn-plugin/src/util.ts b/packages/yarn-plugin/src/util.ts new file mode 100644 index 0000000000..25b760982b --- /dev/null +++ b/packages/yarn-plugin/src/util.ts @@ -0,0 +1,71 @@ +/* + * 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, structUtils } from '@yarnpkg/core'; +import { npath, xfs } from '@yarnpkg/fslib'; +import { valid as semverValid } from 'semver'; +import { getManifestByVersion } from '@backstage/release-manifests'; +import { PROTOCOL } from './constants'; + +export const inferBackstageVersion = (descriptor: Descriptor) => { + const range = structUtils.parseRange(descriptor.range); + + if (range.protocol !== PROTOCOL) { + throw new Error( + `inferBackstageVersion called with unexpected protocol ${range.protocol}`, + ); + } + + let selector = range.selector; + + // For backstage:* we look up the version from backstage.json + if (selector === `*`) { + const backstageJson = xfs.readJsonSync( + npath.toPortablePath('./backstage.json'), + ); + + selector = backstageJson.version; + } + + if (!semverValid(selector)) { + throw new Error( + `Invalid "backstage:" version string found for ${structUtils.stringifyIdent( + descriptor, + )}. Version must be either "backstage:*" or "backstage:", where version is a single Backstage release version.`, + ); + } + + return selector; +}; + +export const inferPackageVersion = async (descriptor: Descriptor) => { + const ident = structUtils.stringifyIdent(descriptor); + const backstageVersion = inferBackstageVersion(descriptor); + + const manifest = await getManifestByVersion({ + version: backstageVersion, + }); + + const manifestEntry = manifest.packages.find( + candidate => candidate.name === ident, + ); + + if (!manifestEntry) { + throw new Error(`Package ${ident} not found in manifest`); + } + + return manifestEntry.version; +}; diff --git a/yarn.lock b/yarn.lock index 199462391b..b8cc660d66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45137,6 +45137,7 @@ __metadata: "@yarnpkg/core": ^4.0.3 "@yarnpkg/fslib": ^3.0.2 nodemon: ^3.0.1 + semver: ^7.6.0 languageName: unknown linkType: soft