Merge pull request #5351 from backstage/rugvip/plugin-detective
core: allow plugins to be discovered through the react tree
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user