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