diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index a70720d23b..fcd3f4d97d 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -91,6 +91,11 @@ export interface BackstagePackageJson { */ pluginPackages?: string[]; + /** + * Package names that this module provides integration for. When any of these packages are used, this module should be suggested as a related integration. + */ + integrationFor?: string[]; + /** * The feature types exported from the package, indexed by path. */ diff --git a/packages/cli/src/modules/maintenance/commands/repo/fix.ts b/packages/cli/src/modules/maintenance/commands/repo/fix.ts index c2c92a9770..adc783936e 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/fix.ts +++ b/packages/cli/src/modules/maintenance/commands/repo/fix.ts @@ -430,6 +430,54 @@ export function fixPluginPackages( } } +export function fixIntegrationFor(pkg: FixablePackage) { + const pkgBackstage = pkg.packageJson.backstage; + const role = pkgBackstage?.role; + if (!role) { + return; + } + + const integrationFor = pkgBackstage.integrationFor; + if (integrationFor === undefined) { + return; + } + + // Validate that integrationFor is only used on module packages + if (role !== 'backend-plugin-module' && role !== 'frontend-plugin-module') { + const path = relativePath( + paths.targetRoot, + resolvePath(pkg.dir, 'package.json'), + ); + throw new Error( + `The 'backstage.integrationFor' field in "${pkg.packageJson.name}" can only be used on module packages (backend-plugin-module or frontend-plugin-module), but package has role '${role}' in "${path}"`, + ); + } + + // Validate that integrationFor is an array + if (!Array.isArray(integrationFor)) { + const path = relativePath( + paths.targetRoot, + resolvePath(pkg.dir, 'package.json'), + ); + throw new Error( + `Invalid 'backstage.integrationFor' field in "${pkg.packageJson.name}", must be an array of package names in "${path}"`, + ); + } + + // Validate that all entries are non-empty strings + for (const entry of integrationFor) { + if (typeof entry !== 'string' || entry.length === 0) { + const path = relativePath( + paths.targetRoot, + resolvePath(pkg.dir, 'package.json'), + ); + throw new Error( + `Invalid entry in 'backstage.integrationFor' field in "${pkg.packageJson.name}", all entries must be non-empty package name strings in "${path}"`, + ); + } + } +} + type PackageFixer = (pkg: FixablePackage, packages: FixablePackage[]) => void; export async function command(opts: OptionValues): Promise { @@ -444,6 +492,7 @@ export async function command(opts: OptionValues): Promise { fixRepositoryField, fixPluginId, fixPluginPackages, + fixIntegrationFor, // Run the publish preflight check too, to make sure we don't uncover errors during publishing publishPreflightCheck, );