Merge pull request #32586 from backstage/rugvip/integration-for-metadata-docs

docs: add backstage.peerModules metadata field
This commit is contained in:
Patrik Oldsberg
2026-02-03 13:55:23 +01:00
committed by GitHub
10 changed files with 114 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/cli': patch
'@backstage/cli-node': patch
---
Added support for the new `peerModules` metadata field in `package.json`. This field allows plugin packages to declare modules that should be installed alongside them for cross-plugin integrations. The field is validated by `backstage-cli repo fix --publish`.
@@ -0,0 +1,8 @@
---
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-notifications-backend': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-techdocs-backend': patch
---
Added `peerModules` metadata declaring recommended modules for cross-plugin integrations.
+39
View File
@@ -168,6 +168,45 @@ The presence of this field is checked by the `backstage-cli package prepack` com
}
```
### `backstage.peerModules`
For plugin packages, this optional field declares modules that should be installed alongside this plugin for cross-plugin integrations. If the peer module's target plugin is present in the Backstage installation, you should typically have the peer module installed as well.
For example, if you use `@backstage/plugin-scaffolder-backend`, you should also install `@backstage/plugin-catalog-backend-module-scaffolder-entity-model` to enable catalog support for scaffolder entity templates. The scaffolder plugin declares this relationship through `peerModules`.
The field uses full package names to allow precise targeting of specific modules. This field can only be used on plugin packages (`backend-plugin` or `frontend-plugin` roles).
```js title="Example usage of the backstage.peerModules field"
{
"name": "@backstage/plugin-scaffolder-backend",
"backstage": {
"role": "backend-plugin",
"pluginId": "scaffolder",
"peerModules": [
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model"
]
}
...
}
```
A plugin can declare multiple peer modules:
```js title="Example of multiple peer modules"
{
"name": "@example/plugin-catalog-backend",
"backstage": {
"role": "backend-plugin",
"pluginId": "catalog",
"peerModules": [
"@example/plugin-search-backend-module-catalog",
"@example/plugin-explore-backend-module-catalog"
]
}
...
}
```
### `backstage.moved`
This field indicates that a package has been renamed and moved to a new location. This field is recognized by the Backstage CLI, where the version bump command will automatically switch to using the new package instead. The value of this field should be the new package name.
+1
View File
@@ -25,6 +25,7 @@ export interface BackstagePackageJson {
pluginId?: string | null;
pluginPackage?: string;
pluginPackages?: string[];
peerModules?: string[];
features?: Record<string, BackstagePackageFeatureType>;
};
// (undocumented)
@@ -91,6 +91,12 @@ export interface BackstagePackageJson {
*/
pluginPackages?: string[];
/**
* Module packages that should be installed alongside this plugin for cross-plugin integrations.
* If the peer module's target plugin is present, you should have the peer module installed.
*/
peerModules?: string[];
/**
* The feature types exported from the package, indexed by path.
*/
@@ -430,6 +430,47 @@ export function fixPluginPackages(
}
}
export function fixPeerModules(pkg: FixablePackage) {
const pkgBackstage = pkg.packageJson.backstage;
const role = pkgBackstage?.role;
if (!role) {
return;
}
const peerModules = pkgBackstage.peerModules;
if (peerModules === undefined) {
return;
}
const packagePath = relativePath(
paths.targetRoot,
resolvePath(pkg.dir, 'package.json'),
);
// Validate that peerModules is only used on plugin packages
if (role !== 'backend-plugin' && role !== 'frontend-plugin') {
throw new Error(
`The 'backstage.peerModules' field in "${pkg.packageJson.name}" can only be used on plugin packages (backend-plugin or frontend-plugin), but package has role '${role}' in "${packagePath}"`,
);
}
// Validate that peerModules is an array
if (!Array.isArray(peerModules)) {
throw new Error(
`Invalid 'backstage.peerModules' field in "${pkg.packageJson.name}", must be an array of package names in "${packagePath}"`,
);
}
// Validate that all entries are non-empty strings
for (const entry of peerModules) {
if (typeof entry !== 'string' || entry.length === 0) {
throw new Error(
`Invalid entry in 'backstage.peerModules' field in "${pkg.packageJson.name}", all entries must be non-empty package name strings in "${packagePath}"`,
);
}
}
}
type PackageFixer = (pkg: FixablePackage, packages: FixablePackage[]) => void;
export async function command(opts: OptionValues): Promise<void> {
@@ -444,6 +485,7 @@ export async function command(opts: OptionValues): Promise<void> {
fixRepositoryField,
fixPluginId,
fixPluginPackages,
fixPeerModules,
// Run the publish preflight check too, to make sure we don't uncover errors during publishing
publishPreflightCheck,
);
+3
View File
@@ -11,6 +11,9 @@
"@backstage/plugin-catalog-common",
"@backstage/plugin-catalog-node",
"@backstage/plugin-catalog-react"
],
"peerModules": [
"@backstage/plugin-search-backend-module-catalog"
]
},
"publishConfig": {
@@ -9,6 +9,9 @@
"@backstage/plugin-notifications-backend",
"@backstage/plugin-notifications-common",
"@backstage/plugin-notifications-node"
],
"peerModules": [
"@backstage/plugin-scaffolder-backend-module-notifications"
]
},
"publishConfig": {
+3
View File
@@ -12,6 +12,9 @@
"@backstage/plugin-scaffolder-node",
"@backstage/plugin-scaffolder-node-test-utils",
"@backstage/plugin-scaffolder-react"
],
"peerModules": [
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model"
]
},
"publishConfig": {
+3
View File
@@ -11,6 +11,9 @@
"@backstage/plugin-techdocs-common",
"@backstage/plugin-techdocs-node",
"@backstage/plugin-techdocs-react"
],
"peerModules": [
"@backstage/plugin-search-backend-module-techdocs"
]
},
"publishConfig": {