diff --git a/.changeset/fresh-cheetahs-rush.md b/.changeset/fresh-cheetahs-rush.md new file mode 100644 index 0000000000..fe8a9caafb --- /dev/null +++ b/.changeset/fresh-cheetahs-rush.md @@ -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. diff --git a/.changeset/good-glasses-build.md b/.changeset/good-glasses-build.md new file mode 100644 index 0000000000..3d506d3e43 --- /dev/null +++ b/.changeset/good-glasses-build.md @@ -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. diff --git a/.changeset/quiet-badgers-cheer.md b/.changeset/quiet-badgers-cheer.md new file mode 100644 index 0000000000..e22bf9492c --- /dev/null +++ b/.changeset/quiet-badgers-cheer.md @@ -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. diff --git a/packages/app/package.json b/packages/app/package.json index 6f58bdb82a..4d2dbe660a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index e08bfd6abe..7dbea5b995 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -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'; diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index affbf64922..9fc5531dfa 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -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); } diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx index c91eef1fb0..3cade002de 100644 --- a/packages/core-api/src/app/App.test.tsx +++ b/packages/core-api/src/app/App.test.tsx @@ -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 () => { diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index d2346e478c..fdbee075fc 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -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; private readonly icons: IconComponentMap; - private readonly plugins: BackstagePlugin[]; + private readonly plugins: Set>; 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[] { - 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 ( - + ) { const pluginIds = new Set(); - 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}'`); diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 1ed7aa030a..026771ce4f 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -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, diff --git a/packages/create-app/templates/default-app/packages/app/src/plugins.ts b/packages/create-app/templates/default-app/packages/app/src/plugins.ts deleted file mode 100644 index df53885723..0000000000 --- a/packages/create-app/templates/default-app/packages/app/src/plugins.ts +++ /dev/null @@ -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'; -