diff --git a/.changeset/chilled-sloths-rest.md b/.changeset/chilled-sloths-rest.md new file mode 100644 index 0000000000..1cad0be99f --- /dev/null +++ b/.changeset/chilled-sloths-rest.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Filters for discovered packages are now also applied at runtime. This makes it possible to disable packages through the `app.experimental.packages` config at runtime. diff --git a/packages/frontend-app-api/config.d.ts b/packages/frontend-app-api/config.d.ts index 81c24d1185..5f5f877d3e 100644 --- a/packages/frontend-app-api/config.d.ts +++ b/packages/frontend-app-api/config.d.ts @@ -16,6 +16,14 @@ export interface Config { app?: { + experimental?: { + /** + * @visibility frontend + * @deepVisibility frontend + */ + packages?: 'all' | { include?: string[]; exclude?: string[] }; + }; + /** * @deepVisibility frontend */ diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index f262ecde72..4b7e4985f3 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -116,7 +116,7 @@ export interface ExtensionTree { export function createExtensionTree(options: { config: Config; }): ExtensionTree { - const features = getAvailableFeatures(); + const features = getAvailableFeatures(options.config); const { instances } = createInstances({ features, config: options.config, @@ -286,7 +286,7 @@ export function createApp(options: { overrideBaseUrlConfigs(defaultConfigLoaderSync()), ); - const discoveredFeatures = getAvailableFeatures(); + const discoveredFeatures = getAvailableFeatures(config); const loadedFeatures = (await options.featureLoader?.({ config })) ?? []; const allFeatures = Array.from( new Set([ diff --git a/packages/frontend-app-api/src/wiring/discovery.test.ts b/packages/frontend-app-api/src/wiring/discovery.test.ts index 54294bbd45..6008ce6746 100644 --- a/packages/frontend-app-api/src/wiring/discovery.test.ts +++ b/packages/frontend-app-api/src/wiring/discovery.test.ts @@ -16,24 +16,29 @@ import { createPlugin } from '@backstage/frontend-plugin-api'; import { getAvailableFeatures } from './discovery'; +import { ConfigReader } from '@backstage/config'; const globalSpy = jest.fn(); Object.defineProperty(global, '__@backstage/discovered__', { get: globalSpy, }); +const config = new ConfigReader({ + app: { experimental: { packages: 'all' } }, +}); + describe('getAvailableFeatures', () => { afterEach(jest.resetAllMocks); it('should discover nothing with undefined global', () => { - expect(getAvailableFeatures()).toEqual([]); + expect(getAvailableFeatures(config)).toEqual([]); }); it('should discover nothing with empty global', () => { globalSpy.mockReturnValue({ modules: [], }); - expect(getAvailableFeatures()).toEqual([]); + expect(getAvailableFeatures(config)).toEqual([]); }); it('should discover a plugin', () => { @@ -41,24 +46,24 @@ describe('getAvailableFeatures', () => { globalSpy.mockReturnValue({ modules: [{ default: testPlugin }], }); - expect(getAvailableFeatures()).toEqual([testPlugin]); + expect(getAvailableFeatures(config)).toEqual([testPlugin]); }); it('should ignore garbage', () => { globalSpy.mockReturnValueOnce({ modules: [{ default: null }] }); - expect(getAvailableFeatures()).toEqual([]); + expect(getAvailableFeatures(config)).toEqual([]); globalSpy.mockReturnValueOnce({ modules: [{ default: undefined }] }); - expect(getAvailableFeatures()).toEqual([]); + expect(getAvailableFeatures(config)).toEqual([]); globalSpy.mockReturnValueOnce({ modules: [{ default: Symbol() }] }); - expect(getAvailableFeatures()).toEqual([]); + expect(getAvailableFeatures(config)).toEqual([]); globalSpy.mockReturnValueOnce({ modules: [{ default: () => {} }] }); - expect(getAvailableFeatures()).toEqual([]); + expect(getAvailableFeatures(config)).toEqual([]); globalSpy.mockReturnValueOnce({ modules: [{ default: 0 }] }); - expect(getAvailableFeatures()).toEqual([]); + expect(getAvailableFeatures(config)).toEqual([]); globalSpy.mockReturnValueOnce({ modules: [{ default: false }] }); - expect(getAvailableFeatures()).toEqual([]); + expect(getAvailableFeatures(config)).toEqual([]); globalSpy.mockReturnValueOnce({ modules: [{ default: true }] }); - expect(getAvailableFeatures()).toEqual([]); + expect(getAvailableFeatures(config)).toEqual([]); }); it('should discover multiple plugins', () => { @@ -72,7 +77,7 @@ describe('getAvailableFeatures', () => { { default: test3Plugin }, ], }); - expect(getAvailableFeatures()).toEqual([ + expect(getAvailableFeatures(config)).toEqual([ test1Plugin, test2Plugin, test3Plugin, diff --git a/packages/frontend-app-api/src/wiring/discovery.ts b/packages/frontend-app-api/src/wiring/discovery.ts index 14786c93d9..1181992d53 100644 --- a/packages/frontend-app-api/src/wiring/discovery.ts +++ b/packages/frontend-app-api/src/wiring/discovery.ts @@ -14,28 +14,75 @@ * limitations under the License. */ +import { Config, ConfigReader } from '@backstage/config'; import { BackstagePlugin, ExtensionOverrides, } from '@backstage/frontend-plugin-api'; interface DiscoveryGlobal { - modules: Array<{ name: string; default: unknown }>; + modules: Array<{ name: string; export?: string; default: unknown }>; +} + +function readPackageDetectionConfig(config: Config) { + const packages = config.getOptional('app.experimental.packages'); + if (packages === undefined || packages === null) { + return undefined; + } + + if (typeof packages === 'string') { + if (packages !== 'all') { + throw new Error( + `Invalid app.experimental.packages mode, got '${packages}', expected 'all'`, + ); + } + return {}; + } + + if (typeof packages !== 'object' || Array.isArray(packages)) { + throw new Error( + "Invalid config at 'app.experimental.packages', expected object", + ); + } + const packagesConfig = new ConfigReader( + packages, + 'app.experimental.packages', + ); + + return { + include: packagesConfig.getOptionalStringArray('include'), + exclude: packagesConfig.getOptionalStringArray('exclude'), + }; } /** * @public */ -export function getAvailableFeatures(): ( - | BackstagePlugin - | ExtensionOverrides -)[] { +export function getAvailableFeatures( + config: Config, +): (BackstagePlugin | ExtensionOverrides)[] { const discovered = ( window as { '__@backstage/discovered__'?: DiscoveryGlobal } )['__@backstage/discovered__']; + const detection = readPackageDetectionConfig(config); + if (!detection) { + return []; + } + return ( - discovered?.modules.map(m => m.default).filter(isBackstageFeature) ?? [] + discovered?.modules + .filter(({ name }) => { + if (detection.exclude?.includes(name)) { + return false; + } + if (detection.include && !detection.include.includes(name)) { + return false; + } + return true; + }) + .map(m => m.default) + .filter(isBackstageFeature) ?? [] ); }