Merge pull request #19674 from UsainBloot/jackpalmer/feature-discovery-package-targetting

backend-app-api: Feature Discovery - include, exclude & alpha modules
This commit is contained in:
Jack Palmer
2023-08-30 17:14:13 +01:00
committed by GitHub
3 changed files with 293 additions and 19 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-app-api': patch
---
Adds include and exclude configuration to feature discovery of backend packages
Adds alpha modules to feature discovery
@@ -34,6 +34,7 @@ describe('featureDiscoveryServiceFactory', () => {
dependencies: {
'detected-plugin': '0.0.0',
'detected-module': '0.0.0',
'detected-plugin-with-alpha': '0.0.0',
},
}),
},
@@ -65,7 +66,7 @@ describe('featureDiscoveryServiceFactory', () => {
name: 'detected-module',
main: 'index.js',
backstage: {
role: 'backend-module',
role: 'backend-plugin-module',
},
}),
'index.js': `
@@ -84,6 +85,39 @@ describe('featureDiscoveryServiceFactory', () => {
});
`,
},
[resolvePath(rootDir, 'node_modules/detected-plugin-with-alpha')]: {
'package.json': JSON.stringify({
name: 'detected-plugin-with-alpha',
main: 'index.js',
exports: {
'.': {
default: 'index.js',
},
'./alpha': {
default: 'alpha.js',
},
'./package.json': './package.json',
},
backstage: {
role: 'backend-plugin',
},
}),
'index.js': `exports.detectedPlugin = undefined;`,
'alpha.js': `
const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api');
exports.detectedPluginAlpha = createBackendPlugin({
pluginId: 'detected-alpha',
register(env) {
env.registerInit({
deps: { identity: coreServices.identity },
async init({ identity }) {
identity.getIdentity('detected-plugin-with-alpha');
},
});
},
});
`,
},
});
});
@@ -91,7 +125,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 +144,191 @@ describe('featureDiscoveryServiceFactory', () => {
expect(fn).toHaveBeenCalledWith('detected-plugin');
expect(fn).toHaveBeenCalledWith('detected-module');
expect(fn).toHaveBeenCalledWith('detected-plugin-with-alpha');
});
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', 'detected-plugin-with-alpha'],
},
},
},
}),
],
});
expect(fn).toHaveBeenCalledWith('detected-plugin');
expect(fn).toHaveBeenCalledWith('detected-plugin-with-alpha');
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('detected-plugin-with-alpha');
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('detected-plugin-with-alpha');
});
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('detected-plugin-with-alpha');
});
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',
'detected-plugin-with-alpha',
],
exclude: ['detected-plugin'],
},
},
},
}),
],
});
expect(fn).not.toHaveBeenCalledWith('detected-plugin');
expect(fn).toHaveBeenCalledWith('detected-module');
expect(fn).toHaveBeenCalledWith('detected-plugin-with-alpha');
});
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();
});
});
@@ -17,6 +17,7 @@
import {
BackendFeature,
RootConfigService,
RootLoggerService,
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
@@ -28,7 +29,7 @@ import { resolve as resolvePath, dirname } from 'path';
import fs from 'fs-extra';
import { BackstagePackageJson } from '@backstage/cli-node';
const LOADED_PACKAGE_ROLES = ['backend-plugin', 'backend-module'];
const LOADED_PACKAGE_ROLES = ['backend-plugin', 'backend-plugin-module'];
/** @internal */
async function findClosestPackageDir(
@@ -58,10 +59,38 @@ async function findClosestPackageDir(
/** @internal */
class PackageDiscoveryService implements FeatureDiscoveryService {
constructor(private readonly config: RootConfigService) {}
constructor(
private readonly config: RootConfigService,
private readonly logger: RootLoggerService,
) {}
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<BackendFeature> }> {
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 +98,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[] = [];
@@ -84,13 +111,33 @@ class PackageDiscoveryService implements FeatureDiscoveryService {
if (!LOADED_PACKAGE_ROLES.includes(depPkg?.backstage?.role ?? '')) {
continue;
}
const depModule = require(require.resolve(name, { paths: [packageDir] }));
for (const exportValue of Object.values(depModule)) {
if (isBackendFeature(exportValue)) {
features.push(exportValue);
}
if (isBackendFeatureFactory(exportValue)) {
features.push(exportValue());
const exportedModulePaths = [
require.resolve(name, {
paths: [packageDir],
}),
];
// Find modules exported as alpha
try {
exportedModulePaths.push(
require.resolve(`${name}/alpha`, { paths: [packageDir] }),
);
} catch {
/* ignore */
}
for (const modulePath of exportedModulePaths) {
const module = require(modulePath);
for (const [exportName, exportValue] of Object.entries(module)) {
if (isBackendFeature(exportValue)) {
this.logger.info(`Detected: ${name}#${exportName}`);
features.push(exportValue);
}
if (isBackendFeatureFactory(exportValue)) {
this.logger.info(`Detected: ${name}#${exportName}`);
features.push(exportValue());
}
}
}
}
@@ -104,9 +151,10 @@ export const featureDiscoveryServiceFactory = createServiceFactory({
service: featureDiscoveryServiceRef,
deps: {
config: coreServices.rootConfig,
logger: coreServices.rootLogger,
},
factory({ config }) {
return new PackageDiscoveryService(config);
factory({ config, logger }) {
return new PackageDiscoveryService(config, logger);
},
});