Merge pull request #5351 from backstage/rugvip/plugin-detective

core: allow plugins to be discovered through the react tree
This commit is contained in:
Patrik Oldsberg
2021-04-16 10:42:30 +02:00
committed by GitHub
10 changed files with 54 additions and 72 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
No longer add newly created plugins to `plugins.ts` in the app, as it is no longer needed.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-api': patch
'@backstage/core': patch
---
Add support for discovering plugins through the app element tree, removing the need to register them explicitly.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/create-app': patch
---
Removed `plugins.ts` from the app, as plugins are now discovered through the react tree.
To apply this change to an existing app, simply delete `packages/app/src/plugins.ts` along with the import and usage in `packages/app/src/App.tsx`.
Note that there are a few plugins that require explicit registration, in which case you would need to keep them in `plugins.ts`. The set of plugins that need explicit registration is any plugin that doesn't have a component extension that gets rendered as part of the app element tree. An example of such a plugin in the main Backstage repo is `@backstage/plugin-badges`. In the case of the badges plugin this is because there is not yet a component-based API for adding context menu items to the entity layout.
If you have plugins that still rely on route registration through the `register` method of `createPlugin`, these need to be kept in `plugins.ts` as well. However, it is recommended to migrate these to export an extensions component instead.
-3
View File
@@ -19,8 +19,6 @@
"@backstage/plugin-explore": "^0.3.2",
"@backstage/plugin-gcp-projects": "^0.2.5",
"@backstage/plugin-github-actions": "^0.4.2",
"@backstage/plugin-github-deployments": "^0.1.2",
"@backstage/plugin-gitops-profiles": "^0.2.6",
"@backstage/plugin-graphiql": "^0.2.9",
"@backstage/plugin-jenkins": "^0.4.1",
"@backstage/plugin-kafka": "^0.2.6",
@@ -29,7 +27,6 @@
"@backstage/plugin-newrelic": "^0.2.6",
"@backstage/plugin-org": "^0.3.12",
"@backstage/plugin-pagerduty": "0.3.2",
"@backstage/plugin-register-component": "^0.2.12",
"@backstage/plugin-rollbar": "^0.3.3",
"@backstage/plugin-scaffolder": "^0.9.0",
"@backstage/plugin-search": "^0.3.4",
+3 -32
View File
@@ -13,36 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
export { catalogPlugin } from '@backstage/plugin-catalog';
export { scaffolderPlugin } from '@backstage/plugin-scaffolder';
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
export { explorePlugin } from '@backstage/plugin-explore';
export { plugin as Circleci } from '@backstage/plugin-circleci';
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
export { plugin as Sentry } from '@backstage/plugin-sentry';
export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles';
export { plugin as TechDocs } from '@backstage/plugin-techdocs';
export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
export { plugin as Newrelic } from '@backstage/plugin-newrelic';
export { travisciPlugin } from '@roadiehq/backstage-plugin-travis-ci';
export { plugin as Jenkins } from '@backstage/plugin-jenkins';
export { plugin as ApiDocs } from '@backstage/plugin-api-docs';
export { githubPullRequestsPlugin } from '@roadiehq/backstage-plugin-github-pull-requests';
export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects';
export { plugin as Kubernetes } from '@backstage/plugin-kubernetes';
export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild';
export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
export { githubInsightsPlugin } from '@roadiehq/backstage-plugin-github-insights';
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
export { plugin as PagerDuty } from '@backstage/plugin-pagerduty';
export { buildkitePlugin } from '@roadiehq/backstage-plugin-buildkite';
export { plugin as Search } from '@backstage/plugin-search';
export { plugin as Org } from '@backstage/plugin-org';
export { plugin as Kafka } from '@backstage/plugin-kafka';
export { todoPlugin } from '@backstage/plugin-todo';
// TODO(Rugvip): This plugin is currently not part of the app element tree,
// ideally we have an API for the context menu that permits that.
export { badgesPlugin } from '@backstage/plugin-badges';
export { githubDeploymentsPlugin } from '@backstage/plugin-github-deployments';
@@ -106,24 +106,6 @@ export async function addPluginDependencyToApp(
});
}
export async function addPluginImportToApp(
rootDir: string,
pluginVar: string,
pluginPackage: string,
) {
const pluginExport = `export { ${pluginVar} } from '${pluginPackage}';`;
const pluginsFilePath = 'packages/app/src/plugins.ts';
const pluginsFile = resolvePath(rootDir, pluginsFilePath);
await Task.forItem('processing', pluginsFilePath, async () => {
await addExportStatement(pluginsFile, pluginExport).catch(error => {
throw new Error(
`Failed to import plugin in app: ${pluginsFile}: ${error.message}`,
);
});
});
}
export async function addPluginExtensionToApp(
pluginId: string,
extensionName: string,
@@ -320,7 +302,6 @@ export default async (cmd: Command) => {
await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion);
Task.section('Import plugin in app');
await addPluginImportToApp(paths.targetRoot, pluginVar, name);
await addPluginExtensionToApp(pluginId, extensionName, name);
}
+3
View File
@@ -212,6 +212,9 @@ describe('Integration Test', () => {
expect(screen.getByText('extLink2: /foo/a')).toBeInTheDocument();
expect(screen.getByText('extLink3: /sub1')).toBeInTheDocument();
expect(screen.getByText('extLink4: /foo/b')).toBeInTheDocument();
// Plugins should be discovered through element tree
expect(app.getPlugins()).toEqual([plugin1, plugin2]);
});
it('runs happy paths without optional routes', async () => {
+26 -7
View File
@@ -52,6 +52,7 @@ import {
} from '../extensions/traversal';
import { IconComponent, IconComponentMap, IconKey } from '../icons';
import { BackstagePlugin } from '../plugin';
import { pluginCollector } from '../plugin/collectors';
import { AnyRoutes } from '../plugin/types';
import { RouteRef, ExternalRouteRef, SubRouteRef } from '../routing';
import {
@@ -189,7 +190,7 @@ export class PrivateAppImpl implements BackstageApp {
private readonly apis: Iterable<AnyApiFactory>;
private readonly icons: IconComponentMap;
private readonly plugins: BackstagePlugin<any, any>[];
private readonly plugins: Set<BackstagePlugin<any, any>>;
private readonly components: AppComponents;
private readonly themes: AppTheme[];
private readonly configLoader?: AppConfigLoader;
@@ -201,7 +202,7 @@ export class PrivateAppImpl implements BackstageApp {
constructor(options: FullAppOptions) {
this.apis = options.apis;
this.icons = options.icons;
this.plugins = options.plugins;
this.plugins = new Set(options.plugins);
this.components = options.components;
this.themes = options.themes;
this.configLoader = options.configLoader;
@@ -210,7 +211,7 @@ export class PrivateAppImpl implements BackstageApp {
}
getPlugins(): BackstagePlugin<any, any>[] {
return this.plugins;
return Array.from(this.plugins);
}
getSystemIcon(key: IconKey): IconComponent | undefined {
@@ -276,7 +277,6 @@ export class PrivateAppImpl implements BackstageApp {
getProvider(): ComponentType<{}> {
const appContext = new AppContextImpl(this);
const apiHolder = this.getApiHolder();
const Provider = ({ children }: PropsWithChildren<{}>) => {
const appThemeApi = useMemo(
@@ -292,11 +292,25 @@ export class PrivateAppImpl implements BackstageApp {
routePaths: routePathCollector,
routeParents: routeParentCollector,
routeObjects: routeObjectCollector,
collectedPlugins: pluginCollector,
},
});
validateRoutes(result.routePaths, result.routeParents);
// TODO(Rugvip): Restructure the public API so that we can get an immediate view of
// the app, rather than having to wait for the provider to render.
// For now we need to push the additional plugins we find during
// collection and then make sure we initialize things afterwards.
result.collectedPlugins.forEach(plugin => this.plugins.add(plugin));
this.verifyPlugins(this.plugins);
// Initialize APIs once all plugins are available
if (this.apiHolder) {
throw new Error('Plugin holder was initialized too soon');
}
this.getApiHolder();
return result;
}, [children]);
@@ -340,7 +354,7 @@ export class PrivateAppImpl implements BackstageApp {
}
return (
<ApiProvider apis={apiHolder}>
<ApiProvider apis={this.getApiHolder()}>
<AppContextProvider appContext={appContext}>
<AppThemeProvider>
<RoutingProvider
@@ -495,10 +509,15 @@ export class PrivateAppImpl implements BackstageApp {
return this.apiHolder;
}
verify() {
/**
* @deprecated
*/
verify() {}
private verifyPlugins(plugins: Iterable<BackstagePlugin>) {
const pluginIds = new Set<string>();
for (const plugin of this.plugins) {
for (const plugin of plugins) {
const id = plugin.getId();
if (pluginIds.has(id)) {
throw new Error(`Duplicate plugin found '${id}'`);
@@ -21,11 +21,9 @@ import { UserSettingsPage } from '@backstage/plugin-user-settings';
import { apis } from './apis';
import { entityPage } from './components/catalog/EntityPage';
import { Root } from './components/Root';
import * as plugins from './plugins';
const app = createApp({
apis,
plugins: Object.values(plugins),
bindRoutes({ bind }) {
bind(catalogPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
@@ -1,9 +0,0 @@
export { plugin as ApiDocs } from '@backstage/plugin-api-docs';
export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
export { plugin as TechDocsPlugin } from '@backstage/plugin-techdocs';
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';