backend-app-api: make feature discovery only discovery default exports

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Camila Belo <camilaibs@gmail.com>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Co-authored-by: Philipp Hugenroth <philipph@spotify.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-06 16:05:43 +02:00
parent 202e52c5e3
commit cb7fc410ed
6 changed files with 108 additions and 104 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': patch
---
The experimental backend feature discovery now only considers default exports from packages. It no longer filters packages to include based on the package role, except that `'cli'` packages are ignored. However, the `"backstage"` field is still required in `package.json`.
+8 -1
View File
@@ -43,7 +43,14 @@ import { UrlReader } from '@backstage/backend-common';
// @public (undocumented)
export interface Backend {
// (undocumented)
add(feature: BackendFeature | (() => BackendFeature)): void;
add(
feature:
| BackendFeature
| (() => BackendFeature)
| Promise<{
default: BackendFeature | (() => BackendFeature);
}>,
): void;
// (undocumented)
start(): Promise<void>;
// (undocumented)
@@ -18,10 +18,6 @@ import mockFs from 'mock-fs';
import { resolve as resolvePath, dirname } from 'path';
import { startTestBackend, mockServices } from '@backstage/backend-test-utils';
import { featureDiscoveryServiceFactory } from './featureDiscoveryServiceFactory';
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
const rootDir = dirname(process.argv[1]);
@@ -35,6 +31,7 @@ describe('featureDiscoveryServiceFactory', () => {
'detected-plugin': '0.0.0',
'detected-module': '0.0.0',
'detected-plugin-with-alpha': '0.0.0',
'detected-library': '0.0.0',
},
}),
},
@@ -48,13 +45,13 @@ describe('featureDiscoveryServiceFactory', () => {
}),
'index.js': `
const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api');
exports.detectedPlugin = createBackendPlugin({
exports.default = createBackendPlugin({
pluginId: 'detected',
register(env) {
env.registerInit({
deps: { identity: coreServices.identity },
async init({ identity }) {
identity.getIdentity('detected-plugin');
deps: { logger: coreServices.rootLogger },
async init({ logger }) {
logger.warn('detected-plugin');
},
});
},
@@ -71,14 +68,14 @@ describe('featureDiscoveryServiceFactory', () => {
}),
'index.js': `
const { createBackendModule, coreServices } = require('@backstage/backend-plugin-api');
exports.detectedModuleDerp = createBackendModule({
exports.default = createBackendModule({
pluginId: 'detected',
moduleId: 'derp',
register(env) {
env.registerInit({
deps: { identity: coreServices.identity },
async init({ identity }) {
identity.getIdentity('detected-module');
deps: { logger: coreServices.rootLogger },
async init({ logger }) {
logger.warn('detected-module');
},
});
},
@@ -102,22 +99,42 @@ describe('featureDiscoveryServiceFactory', () => {
role: 'backend-plugin',
},
}),
'index.js': `exports.detectedPlugin = undefined;`,
'index.js': `exports.default = undefined;`,
'alpha.js': `
const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api');
exports.detectedPluginAlpha = createBackendPlugin({
exports.default = createBackendPlugin({
pluginId: 'detected-alpha',
register(env) {
env.registerInit({
deps: { identity: coreServices.identity },
async init({ identity }) {
identity.getIdentity('detected-plugin-with-alpha');
deps: { logger: coreServices.rootLogger },
async init({ logger }) {
logger.warn('detected-plugin-with-alpha');
},
});
},
});
`,
},
[resolvePath(rootDir, 'node_modules/detected-library')]: {
'package.json': JSON.stringify({
name: 'detected-library',
main: 'index.js',
backstage: {
role: 'node-library',
},
}),
'index.js': `
const { createServiceFactory, createServiceRef, coreServices } = require('@backstage/backend-plugin-api');
exports.default = createServiceFactory({
service: createServiceRef({ id: 'test', scope: 'root' }),
deps: { logger: coreServices.rootLogger },
factory({ logger }) {
logger.warn('detected-library');
return {};
},
});
`,
},
});
});
@@ -126,15 +143,11 @@ describe('featureDiscoveryServiceFactory', () => {
});
it('should detect plugin and module packages when "all" is specified', async () => {
const fn = jest.fn().mockResolvedValue({});
const mock = mockServices.rootLogger.mock({ child: () => mock });
await startTestBackend({
features: [
createServiceFactory({
service: coreServices.identity,
deps: {},
factory: () => ({ getIdentity: fn }),
}),
mock.factory,
featureDiscoveryServiceFactory(),
mockServices.rootConfig.factory({
data: { backend: { packages: 'all' } },
@@ -142,27 +155,28 @@ describe('featureDiscoveryServiceFactory', () => {
],
});
expect(fn).toHaveBeenCalledWith('detected-plugin');
expect(fn).toHaveBeenCalledWith('detected-module');
expect(fn).toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin');
expect(mock.warn).toHaveBeenCalledWith('detected-module');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(mock.warn).toHaveBeenCalledWith('detected-library');
});
it('detects only the packages that are listed as included', async () => {
const fn = jest.fn().mockResolvedValue({});
const mock = mockServices.rootLogger.mock({ child: () => mock });
await startTestBackend({
features: [
createServiceFactory({
service: coreServices.identity,
deps: {},
factory: () => ({ getIdentity: fn }),
}),
mock.factory,
featureDiscoveryServiceFactory(),
mockServices.rootConfig.factory({
data: {
backend: {
packages: {
include: ['detected-plugin', 'detected-plugin-with-alpha'],
include: [
'detected-plugin',
'detected-plugin-with-alpha',
'detected-library',
],
},
},
},
@@ -170,21 +184,18 @@ describe('featureDiscoveryServiceFactory', () => {
],
});
expect(fn).toHaveBeenCalledWith('detected-plugin');
expect(fn).toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(fn).not.toHaveBeenCalledWith('detected-module');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(mock.warn).toHaveBeenCalledWith('detected-library');
expect(mock.warn).not.toHaveBeenCalledWith('detected-module');
});
it('does not detect packages when included is an empty list', async () => {
const fn = jest.fn().mockResolvedValue({});
const mock = mockServices.rootLogger.mock({ child: () => mock });
await startTestBackend({
features: [
createServiceFactory({
service: coreServices.identity,
deps: {},
factory: () => ({ getIdentity: fn }),
}),
mock.factory,
featureDiscoveryServiceFactory(),
mockServices.rootConfig.factory({
data: {
@@ -198,21 +209,18 @@ describe('featureDiscoveryServiceFactory', () => {
],
});
expect(fn).not.toHaveBeenCalledWith('detected-plugin');
expect(fn).not.toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(fn).not.toHaveBeenCalledWith('detected-module');
expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin');
expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(mock.warn).not.toHaveBeenCalledWith('detected-module');
expect(mock.warn).not.toHaveBeenCalledWith('detected-library');
});
it('does not detect an excluded packages', async () => {
const fn = jest.fn().mockResolvedValue({});
const mock = mockServices.rootLogger.mock({ child: () => mock });
await startTestBackend({
features: [
createServiceFactory({
service: coreServices.identity,
deps: {},
factory: () => ({ getIdentity: fn }),
}),
mock.factory,
featureDiscoveryServiceFactory(),
mockServices.rootConfig.factory({
data: {
@@ -226,21 +234,18 @@ describe('featureDiscoveryServiceFactory', () => {
],
});
expect(fn).not.toHaveBeenCalledWith('detected-plugin');
expect(fn).not.toHaveBeenCalledWith('detected-module');
expect(fn).toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin');
expect(mock.warn).not.toHaveBeenCalledWith('detected-module');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(mock.warn).toHaveBeenCalledWith('detected-library');
});
it('does not excluded packages when it is an empty list', async () => {
const fn = jest.fn().mockResolvedValue({});
const mock = mockServices.rootLogger.mock({ child: () => mock });
await startTestBackend({
features: [
createServiceFactory({
service: coreServices.identity,
deps: {},
factory: () => ({ getIdentity: fn }),
}),
mock.factory,
featureDiscoveryServiceFactory(),
mockServices.rootConfig.factory({
data: {
@@ -254,21 +259,18 @@ describe('featureDiscoveryServiceFactory', () => {
],
});
expect(fn).toHaveBeenCalledWith('detected-plugin');
expect(fn).toHaveBeenCalledWith('detected-module');
expect(fn).toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin');
expect(mock.warn).toHaveBeenCalledWith('detected-module');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(mock.warn).toHaveBeenCalledWith('detected-library');
});
it('does not detect packages that are included and excluded', async () => {
const fn = jest.fn().mockResolvedValue({});
const mock = mockServices.rootLogger.mock({ child: () => mock });
await startTestBackend({
features: [
createServiceFactory({
service: coreServices.identity,
deps: {},
factory: () => ({ getIdentity: fn }),
}),
mock.factory,
featureDiscoveryServiceFactory(),
mockServices.rootConfig.factory({
data: {
@@ -287,21 +289,18 @@ describe('featureDiscoveryServiceFactory', () => {
],
});
expect(fn).not.toHaveBeenCalledWith('detected-plugin');
expect(fn).toHaveBeenCalledWith('detected-module');
expect(fn).toHaveBeenCalledWith('detected-plugin-with-alpha');
expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin');
expect(mock.warn).not.toHaveBeenCalledWith('detected-library');
expect(mock.warn).toHaveBeenCalledWith('detected-module');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha');
});
it('does not detect any packages when "packages" is empty', async () => {
const fn = jest.fn().mockResolvedValue({});
const mock = mockServices.rootLogger.mock({ child: () => mock });
await startTestBackend({
features: [
createServiceFactory({
service: coreServices.identity,
deps: {},
factory: () => ({ getIdentity: fn }),
}),
mock.factory,
featureDiscoveryServiceFactory(),
mockServices.rootConfig.factory({
data: { backend: { packages: {} } },
@@ -309,19 +308,15 @@ describe('featureDiscoveryServiceFactory', () => {
],
});
expect(fn).not.toHaveBeenCalled();
expect(mock.warn).not.toHaveBeenCalled();
});
it('does not detect any packages when "packages" is not present', async () => {
const fn = jest.fn().mockResolvedValue({});
const mock = mockServices.rootLogger.mock({ child: () => mock });
await startTestBackend({
features: [
createServiceFactory({
service: coreServices.identity,
deps: {},
factory: () => ({ getIdentity: fn }),
}),
mock.factory,
featureDiscoveryServiceFactory(),
mockServices.rootConfig.factory({
data: { backend: {} },
@@ -329,6 +324,6 @@ describe('featureDiscoveryServiceFactory', () => {
],
});
expect(fn).not.toHaveBeenCalled();
expect(mock.warn).not.toHaveBeenCalled();
});
});
@@ -29,8 +29,6 @@ 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-plugin-module'];
/** @internal */
async function findClosestPackageDir(
searchDir: string,
@@ -108,8 +106,8 @@ class PackageDiscoveryService implements FeatureDiscoveryService {
const depPkg = require(require.resolve(`${name}/package.json`, {
paths: [packageDir],
})) as BackstagePackageJson;
if (!LOADED_PACKAGE_ROLES.includes(depPkg?.backstage?.role ?? '')) {
continue;
if (!depPkg?.backstage || depPkg?.backstage?.role === 'cli') {
continue; // Not a backstage package, ignore
}
const exportedModulePaths = [
@@ -128,16 +126,15 @@ class PackageDiscoveryService implements FeatureDiscoveryService {
}
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());
}
const mod = require(modulePath);
if (isBackendFeature(mod.default)) {
this.logger.info(`Detected: ${name}`);
features.push(mod.default);
}
if (isBackendFeatureFactory(mod.default)) {
this.logger.info(`Detected: ${name}`);
features.push(mod.default());
}
}
}
@@ -91,11 +91,11 @@ export class BackendInitializer {
return Object.fromEntries(result);
}
add(feature: Promise<BackendFeature>) {
add(feature: BackendFeature | Promise<BackendFeature>) {
if (this.#startPromise) {
throw new Error('feature can not be added after the backend has started');
}
this.#registeredFeatures.push(feature);
this.#registeredFeatures.push(Promise.resolve(feature));
}
#addFeature(feature: BackendFeature) {
@@ -34,7 +34,7 @@ export class BackstageBackend implements Backend {
if (isPromise(feature)) {
this.#initializer.add(feature.then(f => unwrapFeature(f.default)));
} else {
this.#initializer.add(Promise.resolve(unwrapFeature(feature)));
this.#initializer.add(unwrapFeature(feature));
}
}