Merge pull request #19787 from backstage/mob/package-discovery

cli,frontend-app-api: add experimental support for frontend package discovery
This commit is contained in:
Patrik Oldsberg
2023-09-06 13:00:19 +02:00
committed by GitHub
19 changed files with 528 additions and 18 deletions
+3 -1
View File
@@ -32,6 +32,7 @@ import {
} from './wiring/parameters';
import { RoutingProvider } from './routing/RoutingContext';
import { RouteRef } from '@backstage/core-plugin-api';
import { getAvailablePlugins } from './wiring/discovery';
/** @public */
export function createApp(options: { plugins: BackstagePlugin[] }): {
@@ -40,11 +41,12 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any);
const builtinExtensions = [CoreRouter];
const discoveredPlugins = getAvailablePlugins();
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
const extensionParams = mergeExtensionParameters({
sources: options.plugins,
sources: [...options.plugins, ...discoveredPlugins],
builtinExtensions,
parameters: readAppExtensionParameters(appConfig),
});
@@ -0,0 +1,118 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createPlugin } from '@backstage/frontend-plugin-api';
import { getAvailablePlugins } from './discovery';
const globalSpy = jest.fn();
Object.defineProperty(global, '__@backstage/discovered__', {
get: globalSpy,
});
describe('getAvailablePlugins', () => {
afterEach(jest.resetAllMocks);
it('should discover nothing with undefined global', () => {
expect(getAvailablePlugins()).toEqual([]);
});
it('should discover nothing with empty global', () => {
globalSpy.mockReturnValue({
modules: [],
});
expect(getAvailablePlugins()).toEqual([]);
});
it('should discover a plugin', () => {
const testPlugin = createPlugin({ id: 'test' });
globalSpy.mockReturnValue({
modules: [
{
module: {
testPlugin,
},
},
],
});
expect(getAvailablePlugins()).toEqual([testPlugin]);
});
it('should ignore garbage', () => {
const testPlugin = createPlugin({ id: 'test' });
globalSpy.mockReturnValue({
modules: [
{
module: {
testPlugin,
a: 'a',
b: null,
c: undefined,
d: Symbol('wat'),
e: () => {},
f: [],
g: {},
h: class {},
i: NaN,
j: Infinity,
k: -Infinity,
l: new Date(),
m: new RegExp('wat'),
n: new Error('wat'),
o: new Map(),
p: new Set(),
q: new WeakMap(),
r: new WeakSet(),
s: new ArrayBuffer(1),
t: new DataView(new ArrayBuffer(1)),
u: false,
v: true,
w: 0,
x: 1,
y: -1,
z: '',
},
},
],
});
expect(getAvailablePlugins()).toEqual([testPlugin]);
});
it('should discover multiple plugins', () => {
const test1Plugin = createPlugin({ id: 'test1' });
const test2Plugin = createPlugin({ id: 'test2' });
const test3Plugin = createPlugin({ id: 'test3' });
globalSpy.mockReturnValue({
modules: [
{
module: {
test1Plugin,
test2Plugin,
},
},
{
module: {
test3Plugin,
},
},
],
});
expect(getAvailablePlugins()).toEqual([
test1Plugin,
test2Plugin,
test3Plugin,
]);
});
});
@@ -0,0 +1,43 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
interface DiscoveryGlobal {
modules: Array<{ name: string; module: object }>;
}
/**
* @public
*/
export function getAvailablePlugins(): BackstagePlugin[] {
const discovered = (
window as { '__@backstage/discovered__'?: DiscoveryGlobal }
)['__@backstage/discovered__'];
return (
discovered?.modules.flatMap(({ module: mod }) =>
Object.values(mod).filter(isBackstagePlugin),
) ?? []
);
}
function isBackstagePlugin(obj: unknown): obj is BackstagePlugin {
if (obj !== null && typeof obj === 'object' && '$$type' in obj) {
return obj.$$type === 'backstage-plugin';
}
return false;
}