yarn-plugin: handle packing step

This commit introduces a beforeWorkspacePacking
hook which converts `backstage:` versions back
into `npm:`` versions when packages are packed for
publish. This allows the `backstage:` protocol to
be used even for packages that are intended to
themselves be published (such as packages in
third-party Backstage plugin monorepos outside
core).

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2024-04-10 12:08:28 +01:00
parent 197a51625c
commit 612d471d29
7 changed files with 185 additions and 46 deletions
+15 -14
View File
@@ -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"
}
}
+17
View File
@@ -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:';
@@ -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,
);
}
}
};
+4
View File
@@ -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],
};
@@ -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<Locator[]> {
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)}`,
),
];
}
+71
View File
@@ -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:<version>", 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;
};
+1
View File
@@ -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