From 032622bdcb2c5662cf59cd5b1e9b95be83e01ab2 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Wed, 30 Aug 2023 13:07:32 +0100 Subject: [PATCH] feat: Add include and exclude to backend.packages discovery Signed-off-by: Jack Palmer --- .../featureDiscoveryServiceFactory.test.ts | 212 +++++++++++++++++- .../alpha/featureDiscoveryServiceFactory.ts | 35 ++- 2 files changed, 240 insertions(+), 7 deletions(-) diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts index cb64491a6e..d1159aec50 100644 --- a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts +++ b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts @@ -34,6 +34,7 @@ describe('featureDiscoveryServiceFactory', () => { dependencies: { 'detected-plugin': '0.0.0', 'detected-module': '0.0.0', + 'another-detected-plugin': '0.0.0', }, }), }, @@ -84,6 +85,29 @@ describe('featureDiscoveryServiceFactory', () => { }); `, }, + [resolvePath(rootDir, 'node_modules/another-detected-plugin')]: { + 'package.json': JSON.stringify({ + name: 'another-detected-plugin', + main: 'index.js', + backstage: { + role: 'backend-plugin', + }, + }), + 'index.js': ` + const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api'); + exports.detectedPlugin = createBackendPlugin({ + pluginId: 'another-detected', + register(env) { + env.registerInit({ + deps: { identity: coreServices.identity }, + async init({ identity }) { + identity.getIdentity('another-detected-plugin'); + }, + }); + }, + }); + `, + }, }); }); @@ -91,7 +115,7 @@ describe('featureDiscoveryServiceFactory', () => { mockFs.restore(); }); - it('should detect plugin and module packages', async () => { + it('should detect plugin and module packages when "all" is specified', async () => { const fn = jest.fn().mockResolvedValue({}); await startTestBackend({ @@ -110,5 +134,191 @@ describe('featureDiscoveryServiceFactory', () => { expect(fn).toHaveBeenCalledWith('detected-plugin'); expect(fn).toHaveBeenCalledWith('detected-module'); + expect(fn).toHaveBeenCalledWith('another-detected-plugin'); + }); + + it('detects only the packages that are listed as included', async () => { + const fn = jest.fn().mockResolvedValue({}); + + await startTestBackend({ + features: [ + createServiceFactory({ + service: coreServices.identity, + deps: {}, + factory: () => ({ getIdentity: fn }), + }), + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + include: ['detected-plugin', 'another-detected-plugin'], + }, + }, + }, + }), + ], + }); + + expect(fn).toHaveBeenCalledWith('detected-plugin'); + expect(fn).toHaveBeenCalledWith('another-detected-plugin'); + expect(fn).not.toHaveBeenCalledWith('detected-module'); + }); + + it('does not detect packages when included is an empty list', async () => { + const fn = jest.fn().mockResolvedValue({}); + + await startTestBackend({ + features: [ + createServiceFactory({ + service: coreServices.identity, + deps: {}, + factory: () => ({ getIdentity: fn }), + }), + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + include: [], + }, + }, + }, + }), + ], + }); + + expect(fn).not.toHaveBeenCalledWith('detected-plugin'); + expect(fn).not.toHaveBeenCalledWith('another-detected-plugin'); + expect(fn).not.toHaveBeenCalledWith('detected-module'); + }); + + it('does not detect an excluded packages', async () => { + const fn = jest.fn().mockResolvedValue({}); + + await startTestBackend({ + features: [ + createServiceFactory({ + service: coreServices.identity, + deps: {}, + factory: () => ({ getIdentity: fn }), + }), + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + exclude: ['detected-plugin', 'detected-module'], + }, + }, + }, + }), + ], + }); + + expect(fn).not.toHaveBeenCalledWith('detected-plugin'); + expect(fn).not.toHaveBeenCalledWith('detected-module'); + expect(fn).toHaveBeenCalledWith('another-detected-plugin'); + }); + + it('does not excluded packages when it is an empty list', async () => { + const fn = jest.fn().mockResolvedValue({}); + + await startTestBackend({ + features: [ + createServiceFactory({ + service: coreServices.identity, + deps: {}, + factory: () => ({ getIdentity: fn }), + }), + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + exclude: [], + }, + }, + }, + }), + ], + }); + + expect(fn).toHaveBeenCalledWith('detected-plugin'); + expect(fn).toHaveBeenCalledWith('detected-module'); + expect(fn).toHaveBeenCalledWith('another-detected-plugin'); + }); + + it('does not detect packages that are included and excluded', async () => { + const fn = jest.fn().mockResolvedValue({}); + + await startTestBackend({ + features: [ + createServiceFactory({ + service: coreServices.identity, + deps: {}, + factory: () => ({ getIdentity: fn }), + }), + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + include: [ + 'detected-plugin', + 'detected-module', + 'another-detected-plugin', + ], + exclude: ['detected-plugin'], + }, + }, + }, + }), + ], + }); + + expect(fn).not.toHaveBeenCalledWith('detected-plugin'); + expect(fn).toHaveBeenCalledWith('detected-module'); + expect(fn).toHaveBeenCalledWith('another-detected-plugin'); + }); + + it('does not detect any packages when "packages" is empty', async () => { + const fn = jest.fn().mockResolvedValue({}); + + await startTestBackend({ + features: [ + createServiceFactory({ + service: coreServices.identity, + deps: {}, + factory: () => ({ getIdentity: fn }), + }), + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { backend: { packages: {} } }, + }), + ], + }); + + expect(fn).not.toHaveBeenCalled(); + }); + + it('does not detect any packages when "packages" is not present', async () => { + const fn = jest.fn().mockResolvedValue({}); + + await startTestBackend({ + features: [ + createServiceFactory({ + service: coreServices.identity, + deps: {}, + factory: () => ({ getIdentity: fn }), + }), + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { backend: {} }, + }), + ], + }); + + expect(fn).not.toHaveBeenCalled(); }); }); diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts index b0d2b7938f..5f12f9c8f5 100644 --- a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts +++ b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts @@ -60,8 +60,33 @@ async function findClosestPackageDir( class PackageDiscoveryService implements FeatureDiscoveryService { constructor(private readonly config: RootConfigService) {} + getDependencyNames(path: string) { + const { dependencies } = require(path) as BackstagePackageJson; + const packagesConfig = this.config.getOptional('backend.packages'); + + const dependencyNames = Object.keys(dependencies || {}); + + if (packagesConfig === 'all') { + return dependencyNames; + } + + const includedPackagesConfig = this.config.getOptionalStringArray( + 'backend.packages.include', + ); + + const includedPackages = includedPackagesConfig + ? new Set(includedPackagesConfig) + : dependencyNames; + const excludedPackagesSet = new Set( + this.config.getOptionalStringArray('backend.packages.exclude'), + ); + + return [...includedPackages].filter(name => !excludedPackagesSet.has(name)); + } + async getBackendFeatures(): Promise<{ features: Array }> { - if (this.config.getOptionalString('backend.packages') !== 'all') { + const packagesConfig = this.config.getOptional('backend.packages'); + if (!packagesConfig || Object.keys(packagesConfig).length === 0) { return { features: [] }; } @@ -69,11 +94,9 @@ class PackageDiscoveryService implements FeatureDiscoveryService { if (!packageDir) { throw new Error('Package discovery failed to find package.json'); } - const { dependencies } = require(resolvePath( - packageDir, - 'package.json', - )) as BackstagePackageJson; - const dependencyNames = Object.keys(dependencies || {}); + const dependencyNames = this.getDependencyNames( + resolvePath(packageDir, 'package.json'), + ); const features: BackendFeature[] = [];