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:
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user